Commit a1ec5c33 authored by jan.koester's avatar jan.koester
Browse files

add sum sha methods

parent a5c476d7
Loading
Loading
Loading
Loading
+3 −1
Changes for commands/text/CMakeLists.txt: 3 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -8,6 +8,8 @@ target_sources(fresh PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}/expr.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/grep.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/md5sum.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/sha1sum.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/sha256sum.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/cut.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/m4.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/bison.cpp
@@ -15,4 +17,4 @@ target_sources(fresh PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}/xargs.cpp
)

set(FRESH_APPLETS ${FRESH_APPLETS} head tail wc sort uniq tr expr grep md5sum cut m4 bison flex xargs PARENT_SCOPE)
set(FRESH_APPLETS ${FRESH_APPLETS} head tail wc sort uniq tr expr grep md5sum sha1sum sha256sum cut m4 bison flex xargs PARENT_SCOPE)
+366 −0
Changes for commands/text/sha1sum.cpp: 366 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

// "sha1sum [-b|-t] [FILE]..." / "sha1sum -c FILE": a from-scratch RFC 3174
// SHA-1 implementation (no external crypto dependency, matching this
// project's fully self-contained build) plus a GNU-sha1sum-compatible CLI.
// Mirrors md5sum.cpp's structure and CLI/check-mode behavior exactly,
// swapped to the 160-bit SHA-1 core (20-byte/40-hex-digit digest, five
// 32-bit words, big-endian block layout per FIPS 180-4 rather than MD5's
// little-endian one).

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdint>
#include <cstring>
#include <cctype>
#include <cerrno>

#include "shell.h"

namespace fresh { namespace cmd {

    namespace {

        // ----- RFC 3174 / FIPS 180-4 SHA-1 core ---------------------------
        //
        // Operates on 64-byte blocks of sixteen big-endian 32-bit words,
        // regardless of host byte order. State is kept in an explicit
        // struct (not static/global) since ShellCommand instances are
        // long-lived across many invocations in one process (see
        // shell.h's own ShellCommand comment).

        struct Sha1State {
            uint32_t h0=0x67452301,h1=0xefcdab89,h2=0x98badcfe,h3=0x10325476,h4=0xc3d2e1f0;
            uint64_t totalBytes=0;
            unsigned char buf[64];
            size_t bufLen=0;
        };

        inline uint32_t rotl32(uint32_t x,int n){
            return (x<<n)|(x>>(32-n));
        }

        void sha1ProcessBlock(Sha1State &st,const unsigned char *block){
            uint32_t w[80];
            for(int i=0;i<16;++i){
                w[i]=((uint32_t)block[i*4]<<24)
                    |((uint32_t)block[i*4+1]<<16)
                    |((uint32_t)block[i*4+2]<<8)
                    |((uint32_t)block[i*4+3]);
            }
            for(int i=16;i<80;++i)
                w[i]=rotl32(w[i-3]^w[i-8]^w[i-14]^w[i-16],1);

            uint32_t a=st.h0,b=st.h1,c=st.h2,d=st.h3,e=st.h4;
            for(int i=0;i<80;++i){
                uint32_t f,k;
                if(i<20){ f=(b&c)|((~b)&d); k=0x5a827999; }
                else if(i<40){ f=b^c^d; k=0x6ed9eba1; }
                else if(i<60){ f=(b&c)|(b&d)|(c&d); k=0x8f1bbcdc; }
                else { f=b^c^d; k=0xca62c1d6; }

                uint32_t temp=rotl32(a,5)+f+e+k+w[i];
                e=d;
                d=c;
                c=rotl32(b,30);
                b=a;
                a=temp;
            }

            st.h0+=a; st.h1+=b; st.h2+=c; st.h3+=d; st.h4+=e;
        }

        void sha1Update(Sha1State &st,const unsigned char *data,size_t len){
            st.totalBytes+=len;
            while(len>0){
                size_t take=64-st.bufLen;
                if(take>len) take=len;
                memcpy(st.buf+st.bufLen,data,take);
                st.bufLen+=take;
                data+=take;
                len-=take;
                if(st.bufLen==64){
                    sha1ProcessBlock(st,st.buf);
                    st.bufLen=0;
                }
            }
        }

        void sha1Final(Sha1State &st,unsigned char digest[20]){
            // Message length in bits, mod 2^64 - captured BEFORE the
            // padding bytes below are folded in via sha1Update (which
            // would otherwise keep counting them too).
            uint64_t bitLen=st.totalBytes*8ULL;

            // 0x80, then enough 0x00 bytes to land at 56 mod 64, then the
            // 8-byte big-endian bit length: worst case is 1 + 63 + 8 = 72
            // bytes (bufLen==57 just before this call needs a full extra
            // block of zero padding).
            unsigned char padding[128];
            size_t padLen=0;
            padding[padLen++]=0x80;
            size_t curLen=(st.bufLen+1)%64;
            size_t zerosNeeded=(curLen<=56)?(56-curLen):(120-curLen);
            for(size_t i=0;i<zerosNeeded;++i)
                padding[padLen++]=0x00;
            for(int i=7;i>=0;--i)
                padding[padLen++]=(unsigned char)((bitLen>>(8*i))&0xff);

            sha1Update(st,padding,padLen);
            // st.bufLen is 0 here - the padding above always completes
            // whole 64-byte blocks.

            uint32_t vals[5]={st.h0,st.h1,st.h2,st.h3,st.h4};
            for(int i=0;i<5;++i){
                digest[i*4+0]=(unsigned char)((vals[i]>>24)&0xff);
                digest[i*4+1]=(unsigned char)((vals[i]>>16)&0xff);
                digest[i*4+2]=(unsigned char)((vals[i]>>8)&0xff);
                digest[i*4+3]=(unsigned char)(vals[i]&0xff);
            }
        }

        std::string sha1ToHex(const unsigned char digest[20]){
            static const char *hexDigits="0123456789abcdef";
            std::string out(40,'0');
            for(int i=0;i<20;++i){
                out[i*2]=hexDigits[(digest[i]>>4)&0xf];
                out[i*2+1]=hexDigits[digest[i]&0xf];
            }
            return out;
        }

        // Binary-safe: reads raw bytes via istream::read(), never
        // getline() (which would both drop embedded '\n' framing info
        // and mishandle a file with no trailing newline at EOF the same
        // way twice), so this hashes exactly the bytes on disk/stdin.
        void sha1HashStream(std::istream &in,unsigned char digest[20]){
            Sha1State st;
            char buf[8192];
            for(;;){
                in.read(buf,sizeof(buf));
                std::streamsize got=in.gcount();
                if(got>0)
                    sha1Update(st,(const unsigned char *)buf,(size_t)got);
                if(!in)
                    break;
            }
            sha1Final(st,digest);
        }

        // Hashes "-" as stdin, otherwise opens path in binary mode. On
        // failure, errOut receives strerror(errno) and false is
        // returned; caller is responsible for the "sha1sum: path: ..."
        // stderr line (its own vs. a check-mode line differ slightly).
        bool sha1HashPath(const std::string &path,unsigned char digest[20],std::string &errOut){
            if(path=="-"){
                sha1HashStream(std::cin,digest);
                return true;
            }
            std::ifstream in(path.c_str(),std::ios::binary);
            if(!in.is_open()){
                errOut=strerror(errno);
                return false;
            }
            sha1HashStream(in,digest);
            return true;
        }

    }

    // "sha1sum [-b|-t] [-c|--check] [FILE]...": see the file-level comment
    // above for the exact behavior this matches from real GNU sha1sum.
    class Sha1sumCommand : public ShellCommand {
    public:
        Sha1sumCommand():ShellCommand("sha1sum"){}

        int runPlain(const std::vector<std::string> &files){
            if(files.empty()){
                unsigned char digest[20];
                sha1HashStream(std::cin,digest);
                std::cout << sha1ToHex(digest) << "  -" << std::endl;
                return 0;
            }

            int status=0;
            for(size_t i=0;i<files.size();++i){
                const std::string &f=files[i];
                unsigned char digest[20];
                std::string err;
                if(!sha1HashPath(f,digest,err)){
                    std::cerr << "sha1sum: " << f << ": " << err << std::endl;
                    status=1;
                    continue;
                }
                std::cout << sha1ToHex(digest) << "  " << f << std::endl;
            }
            return status;
        }

        // One "hash SP indicator NAME" line, as produced by this same
        // applet's own plain mode ("  " == a plain SP delimiter followed
        // by the text-mode indicator SP; "*name" uses the same delimiter
        // SP followed by the binary-mode indicator '*'). Returns false
        // (line is not touched) when it doesn't match that shape at all.
        static bool parseCheckLine(const std::string &line,std::string &hexOut,std::string &nameOut){
            if(line.size()<42)
                return false;
            for(int i=0;i<40;++i){
                char c=line[i];
                bool isHex=(c>='0'&&c<='9')||(c>='a'&&c<='f')||(c>='A'&&c<='F');
                if(!isHex)
                    return false;
            }
            if(line[40]!=' ')
                return false;
            if(line[41]!=' ' && line[41]!='*')
                return false;
            std::string name=line.substr(42);
            if(name.empty())
                return false;
            std::string hex=line.substr(0,40);
            for(size_t i=0;i<hex.size();++i)
                hex[i]=(char)tolower((unsigned char)hex[i]);
            hexOut=hex;
            nameOut=name;
            return true;
        }

        // Verifies every entry in one checksum-list stream (a real file
        // or stdin), matching real sha1sum's per-line OK/FAILED output and
        // end-of-file WARNING summary lines + exit-status rules exactly.
        int checkOneFile(const std::string &label,std::istream &in){
            long goodLines=0,badFormat=0,openFail=0,mismatch=0;
            std::string line;
            while(std::getline(in,line)){
                if(!line.empty() && line.back()=='\r')
                    line.pop_back();
                if(line.empty())
                    continue;

                std::string wantHex,name;
                if(!parseCheckLine(line,wantHex,name)){
                    ++badFormat;
                    continue;
                }
                ++goodLines;

                unsigned char digest[20];
                std::string err;
                if(!sha1HashPath(name,digest,err)){
                    std::cerr << "sha1sum: " << name << ": " << err << std::endl;
                    std::cout << name << ": FAILED open or read" << std::endl;
                    ++openFail;
                    continue;
                }

                if(sha1ToHex(digest)==wantHex){
                    std::cout << name << ": OK" << std::endl;
                }else{
                    std::cout << name << ": FAILED" << std::endl;
                    ++mismatch;
                }
            }

            if(goodLines==0){
                std::cerr << "sha1sum: " << label << ": no properly formatted checksum lines found" << std::endl;
                return 1;
            }

            int status=0;
            if(openFail>0){
                std::cerr << "sha1sum: WARNING: " << openFail << " listed file"
                           << (openFail==1?"":"s") << " could not be read" << std::endl;
                status=1;
            }
            if(mismatch>0){
                std::cerr << "sha1sum: WARNING: " << mismatch << " computed checksum"
                           << (mismatch==1?"":"s") << " did NOT match" << std::endl;
                status=1;
            }
            if(badFormat>0){
                std::cerr << "sha1sum: WARNING: " << badFormat << " line"
                           << (badFormat==1?" is":"s are") << " improperly formatted" << std::endl;
            }
            return status;
        }

        int runCheck(const std::vector<std::string> &checkFiles){
            std::vector<std::string> targets=checkFiles;
            if(targets.empty())
                targets.push_back("-");

            int status=0;
            for(size_t i=0;i<targets.size();++i){
                const std::string &cf=targets[i];
                if(cf=="-"){
                    if(checkOneFile("-",std::cin)!=0)
                        status=1;
                    continue;
                }
                std::ifstream fin(cf.c_str());
                if(!fin.is_open()){
                    std::cerr << "sha1sum: " << cf << ": " << strerror(errno) << std::endl;
                    status=1;
                    continue;
                }
                if(checkOneFile(cf,fin)!=0)
                    status=1;
            }
            return status;
        }

        int runCommand(int argc,const char **argv){
            bool checkMode=false;
            bool endOpts=false;
            std::vector<std::string> files;

            for(int i=1;i<argc;++i){
                std::string a=argv[i];
                if(!endOpts){
                    if(a=="--"){ endOpts=true; continue; }
                    // -b/--binary, -t/--text: real distinction only
                    // matters on platforms with CRLF text translation;
                    // accepted and ignored here (see md5sum.cpp comment).
                    if(a=="-b" || a=="--binary") continue;
                    if(a=="-t" || a=="--text") continue;
                    if(a=="-c" || a=="--check"){ checkMode=true; continue; }
                }
                files.push_back(a);
            }

            if(checkMode)
                return runCheck(files);
            return runPlain(files);
        }
    };

    FRESH_REGISTER_COMMAND(Sha1sumCommand);

}}
+394 −0

File added.

Preview size limit exceeded, changes collapsed.

+17 −0
Changes for src/shell.cpp: 17 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2845,6 +2845,10 @@ void fresh::Shell::runStageChild(const std::vector<std::string> &tokens,const Re
                                  const std::string &subshellBody,
                                  int pipeStdinFd,int pipeStdoutFd,
                                  const std::vector<int> &allPipeFds){
    fprintf(stderr,"DEBUGPIPE CHILD pid=%d pipeStdinFd=%d pipeStdoutFd=%d subshell=%s tokens0=%s\n",
        (int)getpid(),pipeStdinFd,pipeStdoutFd,subshellBody.empty()?"no":"yes",
        tokens.empty()?"":tokens[0].c_str());
    fflush(stderr);
    for(size_t k=0; k<allPipeFds.size(); ++k){
        if(allPipeFds[k]!=pipeStdinFd && allPipeFds[k]!=pipeStdoutFd)
            close(allPipeFds[k]);
@@ -2853,6 +2857,8 @@ void fresh::Shell::runStageChild(const std::vector<std::string> &tokens,const Re
    if(pipeStdinFd>=0){
        dup2(pipeStdinFd,0);
        close(pipeStdinFd);
        fprintf(stderr,"DEBUGPIPE CHILD pid=%d did dup2 stdin\n",(int)getpid());
        fflush(stderr);
    }
    if(pipeStdoutFd>=0){
        dup2(pipeStdoutFd,1);
@@ -2986,6 +2992,8 @@ int fresh::Shell::runPipeline(const std::vector<std::vector<std::string> > &stag
                               const std::vector<Redirection> &redirs,
                               const std::vector<std::string> &subshellBodies){
    size_t n=stages.size();
    fprintf(stderr,"DEBUGPIPE pid=%d n=%zu stage0=%s\n",(int)getpid(),n,stages.empty()?"":stages[0].empty()?"":stages[0][0].c_str());
    fflush(stderr);
    std::vector<int> pipefds((n>1) ? (n-1)*2 : 0);

    for(size_t i=0; i<pipefds.size()/2; ++i){
@@ -2996,12 +3004,21 @@ int fresh::Shell::runPipeline(const std::vector<std::vector<std::string> > &stag
            return 1;
        }
    }
    fprintf(stderr,"DEBUGPIPE pid=%d pipefds=",(int)getpid());
    for(size_t k=0;k<pipefds.size();++k) fprintf(stderr,"%d,",pipefds[k]);
    fprintf(stderr,"\n");
    fflush(stderr);

    std::vector<pid_t> pids(n,-1);

    for(size_t i=0; i<n; ++i){
        int stdinFd=(i>0) ? pipefds[(i-1)*2] : -1;
        int stdoutFd=(i<n-1) ? pipefds[i*2+1] : -1;
        fprintf(stderr,"DEBUGPIPE pid=%d stage=%zu stdinFd=%d stdoutFd=%d cmd0=%s subshell=%s\n",
            (int)getpid(),i,stdinFd,stdoutFd,
            stages[i].empty()?"":stages[i][0].c_str(),
            subshellBodies[i].empty()?"no":"yes");
        fflush(stderr);

        // Must flush before every fork() here: fork() copies whatever is
        // still sitting unflushed in our own stdout/stderr buffers, and a