A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(command): port md5sum, sha1sum, sha256sum from just-bash

Mirror the GNU coreutils-style md5sum / sha1sum / sha256sum trio,
including --check verification mode, the binary marker (`*`) on
checksum lines, and the `WARNING: N computed checksum(s) did NOT
match` summary. Shared logic lives in an internal `checksum` helper
so the three command packages stay one-liners.

Backed by stdlib crypto/md5, crypto/sha1, crypto/sha256 instead of
the just-bash hand-rolled MD5 + WebCrypto fallback.

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 0843b4f1 60263e33

+734
+222
command/internal/checksum/checksum.go
··· 1 + // Package checksum implements the shared logic for the md5sum, sha1sum, 2 + // and sha256sum coreutils. 3 + package checksum 4 + 5 + import ( 6 + "bufio" 7 + "context" 8 + "crypto/md5" 9 + "crypto/sha1" 10 + "crypto/sha256" 11 + "encoding/hex" 12 + "errors" 13 + "fmt" 14 + "hash" 15 + "io" 16 + "path" 17 + "regexp" 18 + "strings" 19 + 20 + "github.com/pborman/getopt/v2" 21 + "mvdan.cc/sh/v3/interp" 22 + "tangled.org/xeiaso.net/kefka/command" 23 + ) 24 + 25 + type Algorithm int 26 + 27 + const ( 28 + MD5 Algorithm = iota 29 + SHA1 30 + SHA256 31 + ) 32 + 33 + func (a Algorithm) new() hash.Hash { 34 + switch a { 35 + case MD5: 36 + return md5.New() 37 + case SHA1: 38 + return sha1.New() 39 + case SHA256: 40 + return sha256.New() 41 + } 42 + panic("checksum: unknown algorithm") 43 + } 44 + 45 + // Config configures a single checksum command implementation. 46 + type Config struct { 47 + Name string 48 + Algorithm Algorithm 49 + Summary string 50 + } 51 + 52 + func (c Config) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 53 + if ec == nil { 54 + return errors.New(c.Name + ": nil ExecContext") 55 + } 56 + 57 + stdout := ec.Stdout 58 + if stdout == nil { 59 + stdout = io.Discard 60 + } 61 + stderr := ec.Stderr 62 + if stderr == nil { 63 + stderr = io.Discard 64 + } 65 + 66 + set := getopt.New() 67 + set.SetProgram(c.Name) 68 + set.SetParameters("[FILE]...") 69 + 70 + usage := func() { 71 + fmt.Fprintf(stderr, "Usage: %s [OPTION]... [FILE]...\n", c.Name) 72 + fmt.Fprintf(stderr, "%s.\n\n", c.Summary) 73 + fmt.Fprint(stderr, " -c, --check read checksums from FILEs and check them\n") 74 + fmt.Fprint(stderr, " --help display this help and exit\n") 75 + } 76 + set.SetUsage(usage) 77 + 78 + check := set.BoolLong("check", 'c', "read checksums from FILEs and check them") 79 + set.BoolLong("binary", 'b', "read in binary mode (ignored)") 80 + set.BoolLong("text", 't', "read in text mode (ignored)") 81 + help := set.BoolLong("help", 0, "display this help and exit") 82 + 83 + if err := set.Getopt(append([]string{c.Name}, args...), nil); err != nil { 84 + fmt.Fprintf(stderr, "%s: %s\n", c.Name, err) 85 + usage() 86 + return interp.ExitStatus(1) 87 + } 88 + 89 + if *help { 90 + usage() 91 + return nil 92 + } 93 + 94 + files := set.Args() 95 + if len(files) == 0 { 96 + files = []string{"-"} 97 + } 98 + 99 + if *check { 100 + return runCheck(c, ec, files, stdout, stderr) 101 + } 102 + return runHash(c, ec, files, stdout, stderr) 103 + } 104 + 105 + func runHash(c Config, ec *command.ExecContext, files []string, stdout, stderr io.Writer) error { 106 + exitCode := 0 107 + for _, file := range files { 108 + data, err := readBinary(ec, file) 109 + if err != nil { 110 + fmt.Fprintf(stderr, "%s: %s: No such file or directory\n", c.Name, file) 111 + exitCode = 1 112 + continue 113 + } 114 + h := c.Algorithm.new() 115 + h.Write(data) 116 + fmt.Fprintf(stdout, "%s %s\n", hex.EncodeToString(h.Sum(nil)), file) 117 + } 118 + if exitCode != 0 { 119 + return interp.ExitStatus(uint8(exitCode)) 120 + } 121 + return nil 122 + } 123 + 124 + var checksumLineRE = regexp.MustCompile(`^([a-fA-F0-9]+)\s+[* ]?(.+)$`) 125 + 126 + func runCheck(c Config, ec *command.ExecContext, files []string, stdout, stderr io.Writer) error { 127 + failed := 0 128 + for _, file := range files { 129 + var content []byte 130 + var err error 131 + if file == "-" { 132 + content, err = readStdin(ec) 133 + } else { 134 + content, err = readFile(ec, file) 135 + } 136 + if err != nil { 137 + fmt.Fprintf(stderr, "%s: %s: No such file or directory\n", c.Name, file) 138 + return interp.ExitStatus(1) 139 + } 140 + 141 + scanner := bufio.NewScanner(strings.NewReader(string(content))) 142 + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) 143 + for scanner.Scan() { 144 + match := checksumLineRE.FindStringSubmatch(scanner.Text()) 145 + if match == nil { 146 + continue 147 + } 148 + expected := strings.ToLower(match[1]) 149 + target := match[2] 150 + 151 + data, err := readBinary(ec, target) 152 + if err != nil { 153 + fmt.Fprintf(stdout, "%s: FAILED open or read\n", target) 154 + failed++ 155 + continue 156 + } 157 + h := c.Algorithm.new() 158 + h.Write(data) 159 + if hex.EncodeToString(h.Sum(nil)) == expected { 160 + fmt.Fprintf(stdout, "%s: OK\n", target) 161 + } else { 162 + fmt.Fprintf(stdout, "%s: FAILED\n", target) 163 + failed++ 164 + } 165 + } 166 + } 167 + 168 + if failed > 0 { 169 + plural := "" 170 + if failed > 1 { 171 + plural = "s" 172 + } 173 + fmt.Fprintf(stdout, "%s: WARNING: %d computed checksum%s did NOT match\n", c.Name, failed, plural) 174 + return interp.ExitStatus(1) 175 + } 176 + return nil 177 + } 178 + 179 + func readBinary(ec *command.ExecContext, file string) ([]byte, error) { 180 + if file == "-" { 181 + return readStdin(ec) 182 + } 183 + return readFile(ec, file) 184 + } 185 + 186 + func readStdin(ec *command.ExecContext) ([]byte, error) { 187 + if ec.Stdin == nil { 188 + return nil, nil 189 + } 190 + return io.ReadAll(ec.Stdin) 191 + } 192 + 193 + func readFile(ec *command.ExecContext, file string) ([]byte, error) { 194 + if ec.FS == nil { 195 + return nil, errors.New("no filesystem") 196 + } 197 + f, err := ec.FS.Open(resolvePath(ec, file)) 198 + if err != nil { 199 + return nil, err 200 + } 201 + defer f.Close() 202 + return io.ReadAll(f) 203 + } 204 + 205 + func resolvePath(ec *command.ExecContext, p string) string { 206 + dir := ec.Dir 207 + if dir == "" { 208 + dir = "." 209 + } 210 + if path.IsAbs(p) { 211 + p = strings.TrimPrefix(p, "/") 212 + if p == "" { 213 + return "." 214 + } 215 + return path.Clean(p) 216 + } 217 + joined := path.Join(dir, p) 218 + if joined == "" { 219 + return "." 220 + } 221 + return joined 222 + }
+20
command/internal/md5sum/md5sum.go
··· 1 + package md5sum 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/xeiaso.net/kefka/command" 7 + "tangled.org/xeiaso.net/kefka/command/internal/checksum" 8 + ) 9 + 10 + type Impl struct{} 11 + 12 + var config = checksum.Config{ 13 + Name: "md5sum", 14 + Algorithm: checksum.MD5, 15 + Summary: "compute MD5 message digest", 16 + } 17 + 18 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 19 + return config.Exec(ctx, ec, args) 20 + }
+278
command/internal/md5sum/md5sum_test.go
··· 1 + package md5sum 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "os" 7 + "strings" 8 + "testing" 9 + 10 + "github.com/go-git/go-billy/v5" 11 + "github.com/go-git/go-billy/v5/memfs" 12 + "tangled.org/xeiaso.net/kefka/command" 13 + ) 14 + 15 + func newFS(t *testing.T) billy.Filesystem { 16 + t.Helper() 17 + fs := memfs.New() 18 + write := func(name string, data []byte) { 19 + f, err := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o644) 20 + if err != nil { 21 + t.Fatal(err) 22 + } 23 + f.Write(data) 24 + f.Close() 25 + } 26 + write("hello.txt", []byte("hello")) 27 + write("world.txt", []byte("world")) 28 + // MD5("hello") = 5d41402abc4b2a76b9719d911017c592 29 + write("good.sums", []byte("5d41402abc4b2a76b9719d911017c592 hello.txt\n")) 30 + write("bad.sums", []byte("00000000000000000000000000000000 hello.txt\n")) 31 + write("mixed.sums", []byte( 32 + "5d41402abc4b2a76b9719d911017c592 hello.txt\n"+ 33 + "00000000000000000000000000000000 world.txt\n")) 34 + write("binary.sums", []byte("5d41402abc4b2a76b9719d911017c592 *hello.txt\n")) 35 + write("missing-target.sums", []byte("5d41402abc4b2a76b9719d911017c592 nope.txt\n")) 36 + return fs 37 + } 38 + 39 + func run(t *testing.T, args []string, stdin string, fs billy.Filesystem) (string, string, error) { 40 + t.Helper() 41 + var stdout, stderr bytes.Buffer 42 + ec := &command.ExecContext{ 43 + Stdin: strings.NewReader(stdin), 44 + Stdout: &stdout, 45 + Stderr: &stderr, 46 + Dir: ".", 47 + FS: fs, 48 + } 49 + err := Impl{}.Exec(context.Background(), ec, args) 50 + return stdout.String(), stderr.String(), err 51 + } 52 + 53 + func TestHash(t *testing.T) { 54 + tests := []struct { 55 + name string 56 + args []string 57 + stdin string 58 + wantStdout string 59 + wantErr bool 60 + }{ 61 + { 62 + name: "stdin", 63 + args: nil, 64 + stdin: "hello", 65 + wantStdout: "5d41402abc4b2a76b9719d911017c592 -\n", 66 + }, 67 + { 68 + name: "stdin via dash", 69 + args: []string{"-"}, 70 + stdin: "hello", 71 + wantStdout: "5d41402abc4b2a76b9719d911017c592 -\n", 72 + }, 73 + { 74 + name: "empty input", 75 + args: nil, 76 + stdin: "", 77 + wantStdout: "d41d8cd98f00b204e9800998ecf8427e -\n", 78 + }, 79 + { 80 + name: "single file", 81 + args: []string{"hello.txt"}, 82 + wantStdout: "5d41402abc4b2a76b9719d911017c592 hello.txt\n", 83 + }, 84 + { 85 + name: "multiple files", 86 + args: []string{"hello.txt", "world.txt"}, 87 + wantStdout: "5d41402abc4b2a76b9719d911017c592 hello.txt\n" + 88 + "7d793037a0760186574b0282f2f435e7 world.txt\n", 89 + }, 90 + { 91 + name: "binary flag accepted", 92 + args: []string{"-b", "hello.txt"}, 93 + wantStdout: "5d41402abc4b2a76b9719d911017c592 hello.txt\n", 94 + }, 95 + { 96 + name: "text flag accepted", 97 + args: []string{"--text", "hello.txt"}, 98 + wantStdout: "5d41402abc4b2a76b9719d911017c592 hello.txt\n", 99 + }, 100 + } 101 + 102 + for _, tt := range tests { 103 + t.Run(tt.name, func(t *testing.T) { 104 + stdout, stderr, err := run(t, tt.args, tt.stdin, newFS(t)) 105 + if tt.wantErr && err == nil { 106 + t.Fatal("expected error, got nil") 107 + } 108 + if !tt.wantErr && err != nil { 109 + t.Fatalf("unexpected error: %v", err) 110 + } 111 + if stderr != "" { 112 + t.Errorf("unexpected stderr: %q", stderr) 113 + } 114 + if stdout != tt.wantStdout { 115 + t.Errorf("stdout = %q, want %q", stdout, tt.wantStdout) 116 + } 117 + }) 118 + } 119 + } 120 + 121 + func TestMissingFile(t *testing.T) { 122 + stdout, stderr, err := run(t, []string{"nope.txt"}, "", newFS(t)) 123 + if err == nil { 124 + t.Fatal("expected error for missing file") 125 + } 126 + if stdout != "" { 127 + t.Errorf("expected empty stdout, got %q", stdout) 128 + } 129 + if !strings.Contains(stderr, "md5sum: nope.txt: No such file or directory") { 130 + t.Errorf("stderr = %q, want missing file message", stderr) 131 + } 132 + } 133 + 134 + func TestMissingFileContinuesOthers(t *testing.T) { 135 + stdout, stderr, err := run(t, []string{"nope.txt", "hello.txt"}, "", newFS(t)) 136 + if err == nil { 137 + t.Fatal("expected error from missing file") 138 + } 139 + if !strings.Contains(stdout, "5d41402abc4b2a76b9719d911017c592 hello.txt") { 140 + t.Errorf("expected hello.txt hash in stdout, got %q", stdout) 141 + } 142 + if !strings.Contains(stderr, "nope.txt: No such file or directory") { 143 + t.Errorf("stderr = %q", stderr) 144 + } 145 + } 146 + 147 + func TestCheck(t *testing.T) { 148 + tests := []struct { 149 + name string 150 + args []string 151 + stdin string 152 + wantStdout string 153 + wantErr bool 154 + }{ 155 + { 156 + name: "all ok", 157 + args: []string{"-c", "good.sums"}, 158 + wantStdout: "hello.txt: OK\n", 159 + }, 160 + { 161 + name: "all failed", 162 + args: []string{"--check", "bad.sums"}, 163 + wantStdout: "hello.txt: FAILED\n" + 164 + "md5sum: WARNING: 1 computed checksum did NOT match\n", 165 + wantErr: true, 166 + }, 167 + { 168 + name: "mixed", 169 + args: []string{"-c", "mixed.sums"}, 170 + wantStdout: "hello.txt: OK\n" + 171 + "world.txt: FAILED\n" + 172 + "md5sum: WARNING: 1 computed checksum did NOT match\n", 173 + wantErr: true, 174 + }, 175 + { 176 + name: "binary marker accepted", 177 + args: []string{"-c", "binary.sums"}, 178 + wantStdout: "hello.txt: OK\n", 179 + }, 180 + { 181 + name: "missing target reported as failed open or read", 182 + args: []string{"-c", "missing-target.sums"}, 183 + wantStdout: "nope.txt: FAILED open or read\n" + 184 + "md5sum: WARNING: 1 computed checksum did NOT match\n", 185 + wantErr: true, 186 + }, 187 + { 188 + name: "check from stdin", 189 + args: []string{"-c"}, 190 + stdin: "5d41402abc4b2a76b9719d911017c592 hello.txt\n", 191 + wantStdout: "hello.txt: OK\n", 192 + }, 193 + } 194 + 195 + for _, tt := range tests { 196 + t.Run(tt.name, func(t *testing.T) { 197 + stdout, _, err := run(t, tt.args, tt.stdin, newFS(t)) 198 + if tt.wantErr && err == nil { 199 + t.Fatal("expected error") 200 + } 201 + if !tt.wantErr && err != nil { 202 + t.Fatalf("unexpected error: %v", err) 203 + } 204 + if stdout != tt.wantStdout { 205 + t.Errorf("stdout = %q, want %q", stdout, tt.wantStdout) 206 + } 207 + }) 208 + } 209 + } 210 + 211 + func TestCheckMultipleFailedPlural(t *testing.T) { 212 + stdout, _, err := run(t, []string{"-c"}, 213 + "00000000000000000000000000000000 hello.txt\n"+ 214 + "00000000000000000000000000000000 world.txt\n", newFS(t)) 215 + if err == nil { 216 + t.Fatal("expected error") 217 + } 218 + if !strings.Contains(stdout, "2 computed checksums did NOT match") { 219 + t.Errorf("expected plural in warning, got %q", stdout) 220 + } 221 + } 222 + 223 + func TestCheckMissingChecksumFile(t *testing.T) { 224 + _, stderr, err := run(t, []string{"-c", "nope.sums"}, "", newFS(t)) 225 + if err == nil { 226 + t.Fatal("expected error") 227 + } 228 + if !strings.Contains(stderr, "md5sum: nope.sums: No such file or directory") { 229 + t.Errorf("stderr = %q", stderr) 230 + } 231 + } 232 + 233 + func TestCheckUppercaseHash(t *testing.T) { 234 + stdout, _, err := run(t, []string{"-c"}, 235 + "5D41402ABC4B2A76B9719D911017C592 hello.txt\n", newFS(t)) 236 + if err != nil { 237 + t.Fatalf("unexpected error: %v", err) 238 + } 239 + if stdout != "hello.txt: OK\n" { 240 + t.Errorf("stdout = %q, want OK", stdout) 241 + } 242 + } 243 + 244 + func TestCheckSkipsNonMatchingLines(t *testing.T) { 245 + stdout, _, err := run(t, []string{"-c"}, 246 + "# this is a comment\n"+ 247 + "\n"+ 248 + "5d41402abc4b2a76b9719d911017c592 hello.txt\n", newFS(t)) 249 + if err != nil { 250 + t.Fatalf("unexpected error: %v", err) 251 + } 252 + if stdout != "hello.txt: OK\n" { 253 + t.Errorf("stdout = %q, want OK", stdout) 254 + } 255 + } 256 + 257 + func TestHelp(t *testing.T) { 258 + stdout, stderr, err := run(t, []string{"--help"}, "", newFS(t)) 259 + if err != nil { 260 + t.Fatalf("unexpected error: %v", err) 261 + } 262 + if stdout != "" { 263 + t.Errorf("help should not write to stdout, got %q", stdout) 264 + } 265 + if !strings.Contains(stderr, "Usage: md5sum [OPTION]... [FILE]...") { 266 + t.Errorf("usage missing from help: %q", stderr) 267 + } 268 + if !strings.Contains(stderr, "-c, --check") { 269 + t.Errorf("check flag missing from help: %q", stderr) 270 + } 271 + } 272 + 273 + func TestUnknownFlag(t *testing.T) { 274 + _, _, err := run(t, []string{"--no-such-flag"}, "", newFS(t)) 275 + if err == nil { 276 + t.Fatal("expected error for unknown flag") 277 + } 278 + }
+20
command/internal/sha1sum/sha1sum.go
··· 1 + package sha1sum 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/xeiaso.net/kefka/command" 7 + "tangled.org/xeiaso.net/kefka/command/internal/checksum" 8 + ) 9 + 10 + type Impl struct{} 11 + 12 + var config = checksum.Config{ 13 + Name: "sha1sum", 14 + Algorithm: checksum.SHA1, 15 + Summary: "compute SHA1 message digest", 16 + } 17 + 18 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 19 + return config.Exec(ctx, ec, args) 20 + }
+84
command/internal/sha1sum/sha1sum_test.go
··· 1 + package sha1sum 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "os" 7 + "strings" 8 + "testing" 9 + 10 + "github.com/go-git/go-billy/v5" 11 + "github.com/go-git/go-billy/v5/memfs" 12 + "tangled.org/xeiaso.net/kefka/command" 13 + ) 14 + 15 + func newFS(t *testing.T) billy.Filesystem { 16 + t.Helper() 17 + fs := memfs.New() 18 + f, err := fs.OpenFile("hello.txt", os.O_CREATE|os.O_WRONLY, 0o644) 19 + if err != nil { 20 + t.Fatal(err) 21 + } 22 + f.Write([]byte("hello")) 23 + f.Close() 24 + return fs 25 + } 26 + 27 + func run(t *testing.T, args []string, stdin string) (string, string, error) { 28 + t.Helper() 29 + var stdout, stderr bytes.Buffer 30 + ec := &command.ExecContext{ 31 + Stdin: strings.NewReader(stdin), 32 + Stdout: &stdout, 33 + Stderr: &stderr, 34 + Dir: ".", 35 + FS: newFS(t), 36 + } 37 + err := Impl{}.Exec(context.Background(), ec, args) 38 + return stdout.String(), stderr.String(), err 39 + } 40 + 41 + func TestHashStdin(t *testing.T) { 42 + // SHA1("hello") = aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d 43 + stdout, _, err := run(t, nil, "hello") 44 + if err != nil { 45 + t.Fatalf("unexpected error: %v", err) 46 + } 47 + if want := "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d -\n"; stdout != want { 48 + t.Errorf("stdout = %q, want %q", stdout, want) 49 + } 50 + } 51 + 52 + func TestHashFile(t *testing.T) { 53 + stdout, _, err := run(t, []string{"hello.txt"}, "") 54 + if err != nil { 55 + t.Fatalf("unexpected error: %v", err) 56 + } 57 + if want := "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d hello.txt\n"; stdout != want { 58 + t.Errorf("stdout = %q, want %q", stdout, want) 59 + } 60 + } 61 + 62 + func TestCheckOK(t *testing.T) { 63 + stdout, _, err := run(t, []string{"-c"}, 64 + "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d hello.txt\n") 65 + if err != nil { 66 + t.Fatalf("unexpected error: %v", err) 67 + } 68 + if stdout != "hello.txt: OK\n" { 69 + t.Errorf("stdout = %q, want OK", stdout) 70 + } 71 + } 72 + 73 + func TestHelp(t *testing.T) { 74 + _, stderr, err := run(t, []string{"--help"}, "") 75 + if err != nil { 76 + t.Fatalf("unexpected error: %v", err) 77 + } 78 + if !strings.Contains(stderr, "Usage: sha1sum [OPTION]... [FILE]...") { 79 + t.Errorf("usage missing: %q", stderr) 80 + } 81 + if !strings.Contains(stderr, "compute SHA1 message digest") { 82 + t.Errorf("summary missing: %q", stderr) 83 + } 84 + }
+20
command/internal/sha256sum/sha256sum.go
··· 1 + package sha256sum 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/xeiaso.net/kefka/command" 7 + "tangled.org/xeiaso.net/kefka/command/internal/checksum" 8 + ) 9 + 10 + type Impl struct{} 11 + 12 + var config = checksum.Config{ 13 + Name: "sha256sum", 14 + Algorithm: checksum.SHA256, 15 + Summary: "compute SHA256 message digest", 16 + } 17 + 18 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 19 + return config.Exec(ctx, ec, args) 20 + }
+84
command/internal/sha256sum/sha256sum_test.go
··· 1 + package sha256sum 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "os" 7 + "strings" 8 + "testing" 9 + 10 + "github.com/go-git/go-billy/v5" 11 + "github.com/go-git/go-billy/v5/memfs" 12 + "tangled.org/xeiaso.net/kefka/command" 13 + ) 14 + 15 + func newFS(t *testing.T) billy.Filesystem { 16 + t.Helper() 17 + fs := memfs.New() 18 + f, err := fs.OpenFile("hello.txt", os.O_CREATE|os.O_WRONLY, 0o644) 19 + if err != nil { 20 + t.Fatal(err) 21 + } 22 + f.Write([]byte("hello")) 23 + f.Close() 24 + return fs 25 + } 26 + 27 + func run(t *testing.T, args []string, stdin string) (string, string, error) { 28 + t.Helper() 29 + var stdout, stderr bytes.Buffer 30 + ec := &command.ExecContext{ 31 + Stdin: strings.NewReader(stdin), 32 + Stdout: &stdout, 33 + Stderr: &stderr, 34 + Dir: ".", 35 + FS: newFS(t), 36 + } 37 + err := Impl{}.Exec(context.Background(), ec, args) 38 + return stdout.String(), stderr.String(), err 39 + } 40 + 41 + func TestHashStdin(t *testing.T) { 42 + // SHA256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 43 + stdout, _, err := run(t, nil, "hello") 44 + if err != nil { 45 + t.Fatalf("unexpected error: %v", err) 46 + } 47 + if want := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -\n"; stdout != want { 48 + t.Errorf("stdout = %q, want %q", stdout, want) 49 + } 50 + } 51 + 52 + func TestHashFile(t *testing.T) { 53 + stdout, _, err := run(t, []string{"hello.txt"}, "") 54 + if err != nil { 55 + t.Fatalf("unexpected error: %v", err) 56 + } 57 + if want := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hello.txt\n"; stdout != want { 58 + t.Errorf("stdout = %q, want %q", stdout, want) 59 + } 60 + } 61 + 62 + func TestCheckOK(t *testing.T) { 63 + stdout, _, err := run(t, []string{"-c"}, 64 + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hello.txt\n") 65 + if err != nil { 66 + t.Fatalf("unexpected error: %v", err) 67 + } 68 + if stdout != "hello.txt: OK\n" { 69 + t.Errorf("stdout = %q, want OK", stdout) 70 + } 71 + } 72 + 73 + func TestHelp(t *testing.T) { 74 + _, stderr, err := run(t, []string{"--help"}, "") 75 + if err != nil { 76 + t.Fatalf("unexpected error: %v", err) 77 + } 78 + if !strings.Contains(stderr, "Usage: sha256sum [OPTION]... [FILE]...") { 79 + t.Errorf("usage missing: %q", stderr) 80 + } 81 + if !strings.Contains(stderr, "compute SHA256 message digest") { 82 + t.Errorf("summary missing: %q", stderr) 83 + } 84 + }
+6
command/registry/coreutils/coreutils.go
··· 22 22 "tangled.org/xeiaso.net/kefka/command/internal/hostname" 23 23 "tangled.org/xeiaso.net/kefka/command/internal/join" 24 24 "tangled.org/xeiaso.net/kefka/command/internal/ls" 25 + "tangled.org/xeiaso.net/kefka/command/internal/md5sum" 25 26 "tangled.org/xeiaso.net/kefka/command/internal/mkdir" 26 27 "tangled.org/xeiaso.net/kefka/command/internal/mv" 27 28 "tangled.org/xeiaso.net/kefka/command/internal/nl" 28 29 "tangled.org/xeiaso.net/kefka/command/internal/paste" 30 + "tangled.org/xeiaso.net/kefka/command/internal/sha1sum" 31 + "tangled.org/xeiaso.net/kefka/command/internal/sha256sum" 29 32 "tangled.org/xeiaso.net/kefka/command/internal/truecmd" 30 33 "tangled.org/xeiaso.net/kefka/command/internal/unexpand" 31 34 "tangled.org/xeiaso.net/kefka/command/internal/zcat" ··· 54 57 reg.Register("hostname", hostname.Impl{}) 55 58 reg.Register("join", join.Impl{}) 56 59 reg.Register("ls", ls.Impl{}) 60 + reg.Register("md5sum", md5sum.Impl{}) 57 61 reg.Register("mkdir", mkdir.Impl{}) 58 62 reg.Register("mv", mv.Impl{}) 59 63 reg.Register("nl", nl.Impl{}) 60 64 reg.Register("paste", paste.Impl{}) 65 + reg.Register("sha1sum", sha1sum.Impl{}) 66 + reg.Register("sha256sum", sha256sum.Impl{}) 61 67 reg.Register("true", truecmd.Impl{}) 62 68 reg.Register("unexpand", unexpand.Impl{}) 63 69 reg.Register("zcat", zcat.Impl{})