diff --git a/.github/workflows/go-arcadium-test.yml b/.github/workflows/go-arcadium-test.yml index 81f9136..5779103 100644 --- a/.github/workflows/go-arcadium-test.yml +++ b/.github/workflows/go-arcadium-test.yml @@ -12,7 +12,7 @@ jobs: go-arcadium-test: strategy: matrix: - go: [ '1.26.x' ] + go: [ '1.27.x' ] runs-on: 'ubuntu-latest' diff --git a/src/go/arcadium/Makefile b/src/go/arcadium/Makefile index 10e0b34..168c38d 100644 --- a/src/go/arcadium/Makefile +++ b/src/go/arcadium/Makefile @@ -146,5 +146,5 @@ dist/$(os)/$(arch): clean: @printf "\nClean...\n" - -go clean -testcache -cache + -go clean -i -r -cache -testcache -modcache -fuzzcache -rm -rf dist diff --git a/src/go/arcadium/entropylex/cmd/entropylex/command.go b/src/go/arcadium/entropylex/cmd/entropylex/command.go index 6be22c3..45a797f 100644 --- a/src/go/arcadium/entropylex/cmd/entropylex/command.go +++ b/src/go/arcadium/entropylex/cmd/entropylex/command.go @@ -24,14 +24,14 @@ package main import ( "context" - "errors" "fmt" "net/mail" "os" "path/filepath" - "github.com/AlphaPixel/EntropyLex/src/go/arcadium/build" "github.com/urfave/cli/v3" + + "github.com/AlphaPixel/EntropyLex/src/go/arcadium/build" ) func NewCommand(info build.Information) *cli.Command { @@ -76,13 +76,6 @@ non-empty output file exists, it can be overwritten using the -force option. Aliases: []string{"b"}, Usage: "encoder bit depth, possible values are 8, 12, 14 or 16", Value: 8, - Validator: func(i uint) error { - switch i { - case 8, 12, 14, 16: - return nil - } - return errors.New("possible values are 8, 12, 14 or 16") - }, }, &cli.StringFlag{ Name: "output", @@ -97,13 +90,22 @@ non-empty output file exists, it can be overwritten using the -force option. }, Action: func(ctx context.Context, cmd *cli.Command) error { + // Validate the bit depth. + bitDepth := cmd.Uint("bit-depth") + switch bitDepth { + case 8: + break + case 12, 14, 16: + return fmt.Errorf("%w: bit depth %d unimplemented", ErrUnimplemented, bitDepth) + default: + return fmt.Errorf("%w: invalid bit depth \"%d\", possible values are 8, 12, 14 or 16", ErrUsage, bitDepth) + } + // Setup the output. outfile := os.Stdout f, err := OutputFile(cmd.String("output"), cmd.Bool("force")) if err != nil { - fmt.Fprintf(os.Stderr, "Incorrect Usage: %v\n\n", err) - _ = cli.DefaultShowRootCommandHelp(cmd) - return err + return fmt.Errorf("%w: %w", ErrUsage, err) } if f != nil { outfile = f @@ -114,16 +116,12 @@ non-empty output file exists, it can be overwritten using the -force option. infile := os.Stdin switch { case cmd.NArg() > 1: - fmt.Fprintf(os.Stderr, "Incorrect Usage: extra input file \"%s\"\n\n", cmd.Args().Get(1)) - _ = cli.DefaultShowRootCommandHelp(cmd) - return errors.New("usage error") + return fmt.Errorf("%w: extra input file \"%s\"", ErrUsage, cmd.Args().Get(1)) case cmd.NArg() == 1: filename := cmd.Args().Get(0) f, err := InputFile(filename) if err != nil { - fmt.Fprintf(os.Stderr, "Incorrect Usage: %v\n\n", err) - _ = cli.DefaultShowRootCommandHelp(cmd) - return err + return fmt.Errorf("%w: %w", ErrUsage, err) } if f != nil { infile = f @@ -132,20 +130,13 @@ non-empty output file exists, it can be overwritten using the -force option. } // Are we encoding or decoding? - decode := cmd.Bool("decode") - var el runner - bitDepth := cmd.Uint("bit-depth") switch bitDepth { case 8: - el, err = NewEntropyLex8(infile, outfile, decode) + el, err = NewEntropyLex8(infile, outfile, cmd.Bool("decode")) if err != nil { return err } - case 12, 14, 16: - return fmt.Errorf("bit depth %d unimplemented", bitDepth) - default: - return errors.New("usage error") } return el.Run(ctx) @@ -154,61 +145,3 @@ non-empty output file exists, it can be overwritten using the -force option. return cmd } - -// OutputFile creates an output file give the output filename and the force flag. -func OutputFile(filename string, force bool) (*os.File, error) { - if filename == "" { - return nil, nil - } - - // See if the file exists. If not Create it. - fs, err := os.Stat(filename) - if err != nil { - return os.Create(filename) - } - mode := fs.Mode() - - // If the file exists and it isn't a regular file, return an error. - if !mode.IsRegular() { - errmsg := fmt.Sprintf("output file \"%s\" is not a regular file", filename) - if mode.IsDir() { - errmsg = fmt.Sprintf("output file \"%s\" is a directory", filename) - } - return nil, errors.New(errmsg) - } - - // If the file exists and it's a regular file, create it if the size is 0. - if fs.Size() == 0 { - return os.Create(filename) - } - - // If the size is non-zero and the force flag isn't present, return an error. - if !force { - return nil, errors.New("a non-empty output file exist, to overwrite use the --force option") - } - - return os.Create(filename) -} - -// InputFile opens the input file for reading given the input filename. -func InputFile(filename string) (*os.File, error) { - if filename == "" || filename == "-" { - return nil, nil - } - - fs, err := os.Stat(filename) - if err != nil { - return nil, err - } - mode := fs.Mode() - - if !mode.IsRegular() { - errmsg := fmt.Sprintf("input file \"%s\" is not a regular file", filename) - if mode.IsDir() { - errmsg = fmt.Sprintf("input file \"%s\" is a directory", filename) - } - return nil, errors.New(errmsg) - } - - return os.Open(filename) -} diff --git a/src/go/arcadium/entropylex/cmd/entropylex/command_test.go b/src/go/arcadium/entropylex/cmd/entropylex/command_test.go index 6cd81d0..25506b2 100644 --- a/src/go/arcadium/entropylex/cmd/entropylex/command_test.go +++ b/src/go/arcadium/entropylex/cmd/entropylex/command_test.go @@ -2,12 +2,9 @@ package main_test import ( "context" - "fmt" "os" "testing" - "github.com/google/uuid" - "github.com/AlphaPixel/EntropyLex/src/go/arcadium/build" el "github.com/AlphaPixel/EntropyLex/src/go/arcadium/entropylex/cmd/entropylex" "github.com/AlphaPixel/EntropyLex/src/go/arcadium/test/assert" @@ -48,11 +45,16 @@ func Test_NewCommand(t *testing.T) { name: "invalid bit depth", info: build.Info("name", "version", "branch", "commit", "date"), args: []string{"cmd", "-b", "42"}, - verify: func(t *testing.T, err error, outfile string) { - assert.Error(t, err, "invalid value \"42\" for flag -b: possible values are 8, 12, 14 or 16") - output, err := os.ReadFile(outfile) - assert.Nil(t, err) - assert.Contains(t, string(output), `Incorrect Usage: invalid value "42" for flag -b: possible values are 8, 12, 14 or 16`) + verify: func(t *testing.T, err error, _ string) { + assert.Error(t, err, `usage error: invalid bit depth "42", possible values are 8, 12, 14 or 16`) + }, + }, + { + name: "unimplemented bit depth", + info: build.Info("name", "version", "branch", "commit", "date"), + args: []string{"cmd", "-b", "16"}, + verify: func(t *testing.T, err error, _ string) { + assert.Error(t, err, `unimplemented: bit depth 16 unimplemented`) }, }, // --output @@ -60,24 +62,17 @@ func Test_NewCommand(t *testing.T) { name: "existing file, w/o force", info: build.Info("name", "version", "branch", "commit", "date"), args: []string{"cmd", "-o", "test/output"}, - verify: func(t *testing.T, err error, outfile string) { - assert.Error(t, err, "a non-empty output file exist, to overwrite use the --force option") - output, err := os.ReadFile(outfile) - assert.Nil(t, err) - assert.Contains(t, string(output), `Incorrect Usage: a non-empty output file exist, to overwrite use the --force option`) + verify: func(t *testing.T, err error, _ string) { + assert.Error(t, err, "usage error: a non-empty output file exists, to overwrite use the --force option") }, }, - // FILE { name: "multiple filename args", info: build.Info("name", "version", "branch", "commit", "date"), args: []string{"cmd", "-b", "8", "foo", "bar"}, - verify: func(t *testing.T, err error, outfile string) { - assert.Error(t, err, "usage error") - output, err := os.ReadFile(outfile) - assert.Nil(t, err) - assert.Contains(t, string(output), `Incorrect Usage: extra input file "bar"`) + verify: func(t *testing.T, err error, _ string) { + assert.Error(t, err, `usage error: extra input file "bar"`) }, }, { @@ -85,7 +80,7 @@ func Test_NewCommand(t *testing.T) { info: build.Info("name", "version", "branch", "commit", "date"), args: []string{"cmd", "dict"}, verify: func(t *testing.T, err error, outfile string) { - assert.Error(t, err, `input file "dict" is a directory`) + assert.Error(t, err, `usage error: input file "dict" is a directory`) }, }, } @@ -111,219 +106,3 @@ func Test_NewCommand(t *testing.T) { }) } } - -func Test_OutputFile(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - filename string - force bool - before func(*testing.T, string) - verify func(*testing.T, *os.File, error) - after func(*testing.T, string) - }{ - { - name: "empty filename", - filename: "", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, err) - assert.Nil(t, f) - }, - }, - { - name: "file does not exist", - filename: fmt.Sprintf("test/%s", uuid.NewString()), - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, err) - assert.NotNil(t, f) - assert.Nil(t, f.Close()) - }, - after: func(t *testing.T, output string) { - assert.Nil(t, os.Remove(output)) - }, - }, - { - name: "file does not exist, it non-existant path", - filename: "test/foo/bar/this_should_fail", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, "open test/foo/bar/this_should_fail: no such file or directory") - }, - }, - { - name: "file exists, directory", - filename: "test", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, `output file "test" is a directory`) - }, - }, - { - name: "file exists, bad link", - filename: "test/bad_link", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, "open test/bad_link: no such file or directory") - }, - }, - { - name: "file exists, empty", - filename: "test/output_empty", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, err) - assert.NotNil(t, f) - fs, e := f.Stat() - assert.Nil(t, e) - assert.Equal(t, fs.Size(), 0) - }, - }, - { - name: "file exists, not empty w/o force", - filename: fmt.Sprintf("test/%s", uuid.NewString()), - force: false, - before: func(t *testing.T, output string) { - f, err := os.Create(output) - assert.NotNil(t, f) - assert.Nil(t, err) - _, err = f.Write([]byte("testing 1 2 3 4")) - assert.Nil(t, err) - assert.Nil(t, f.Close()) - }, - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, "a non-empty output file exist, to overwrite use the --force option") - }, - after: func(t *testing.T, output string) { - assert.Nil(t, os.Remove(output)) - }, - }, - { - name: "file exists, not empty w/force", - filename: fmt.Sprintf("test/%s", uuid.NewString()), - force: true, - before: func(t *testing.T, output string) { - f, err := os.Create(output) - assert.NotNil(t, f) - assert.Nil(t, err) - _, err = f.Write([]byte("testing 1 2 3 4")) - assert.Nil(t, err) - assert.Nil(t, f.Close()) - }, - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, err) - assert.NotNil(t, f) - fs, e := f.Stat() - assert.Nil(t, e) - assert.Equal(t, fs.Size(), 0) - assert.Nil(t, f.Close()) - }, - after: func(t *testing.T, output string) { - assert.Nil(t, os.Remove(output)) - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - if test.before != nil { - test.before(t, test.filename) - } - - f, err := el.OutputFile(test.filename, test.force) - test.verify(t, f, err) - - if test.after != nil { - test.after(t, test.filename) - } - }) - } -} - -func Test_InputFile(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - filename string - verify func(*testing.T, *os.File, error) - }{ - { - name: "empty filename", - filename: "", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, err) - assert.Nil(t, f) - }, - }, - { - name: "- filename", - filename: "-", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, err) - assert.Nil(t, f) - }, - }, - { - name: "unknown filename", - filename: "xyq.pdq", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, "stat xyq.pdq: no such file or directory") - }, - }, - { - name: "directory", - filename: "test", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, `input file "test" is a directory`) - }, - }, - { - name: "link to directory", - filename: "./test/dir_link", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, `input file "./test/dir_link" is a directory`) - }, - }, - { - name: "bad link", - filename: "./test/bad_link", - verify: func(t *testing.T, f *os.File, err error) { - assert.Nil(t, f) - assert.Error(t, err, "stat ./test/bad_link: no such file or directory") - }, - }, - { - name: "good file", - filename: "main.go", - verify: func(t *testing.T, f *os.File, err error) { - assert.NotNil(t, f) - assert.Nil(t, err) - assert.Nil(t, f.Close()) - }, - }, - { - name: "link to good file", - filename: "test/good_link", - verify: func(t *testing.T, f *os.File, err error) { - assert.NotNil(t, f) - assert.Nil(t, err) - assert.Nil(t, f.Close()) - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - f, err := el.InputFile(test.filename) - test.verify(t, f, err) - }) - } -} diff --git a/src/go/arcadium/entropylex/cmd/entropylex/entropylex8.go b/src/go/arcadium/entropylex/cmd/entropylex/entropylex8.go index ea7b1bc..6d23caa 100644 --- a/src/go/arcadium/entropylex/cmd/entropylex/entropylex8.go +++ b/src/go/arcadium/entropylex/cmd/entropylex/entropylex8.go @@ -24,7 +24,7 @@ package main import ( "context" - "errors" + "fmt" "io" "github.com/AlphaPixel/EntropyLex/src/go/arcadium/entropylex/dictionary" @@ -46,17 +46,17 @@ type ( func NewEntropyLex8(input io.ReadCloser, output io.WriteCloser, decode bool) (*EntropyLex8, error) { lxjfile, err := dictFS.Open(el8LXJFile) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to open dictionary \"%s\", %w: %w", el8LXJFile, err, ErrInternal) } lxjf, ok := lxjfile.(io.ReadSeekCloser) if !ok { - return nil, errors.New("internal error: failed to load default lxj dictionary") + return nil, fmt.Errorf("failed to load default lxj dictionary: %w", ErrInternal) } lxj, err := dictionary.NewLXJValidated(lxjf) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: %w", err, ErrInternal) } return &EntropyLex8{ diff --git a/src/go/arcadium/entropylex/cmd/entropylex/entropylex8_test.go b/src/go/arcadium/entropylex/cmd/entropylex/entropylex8_test.go new file mode 100644 index 0000000..c49bd7a --- /dev/null +++ b/src/go/arcadium/entropylex/cmd/entropylex/entropylex8_test.go @@ -0,0 +1,9 @@ +package main_test + +import ( + "testing" +) + +func Test_NewEntropyLex8(t *testing.T) { + // TODO +} diff --git a/src/go/arcadium/entropylex/cmd/entropylex/error_code.go b/src/go/arcadium/entropylex/cmd/entropylex/error_code.go new file mode 100644 index 0000000..34e505a --- /dev/null +++ b/src/go/arcadium/entropylex/cmd/entropylex/error_code.go @@ -0,0 +1,51 @@ +// MIT License +// +// Copyright 2026 arcadium.dev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package main + +type ( + ErrorCode struct { + msg string + code int + } +) + +func (err ErrorCode) Error() string { + return err.msg +} + +func (err ErrorCode) Code() int { + return err.code +} + +const ( + UsageErrorCode int = 1 + InternalErrorCode int = 4 + UnknownErrorCode int = 9 + UnimplementedErrorCode int = 111 +) + +var ( + ErrUsage = ErrorCode{msg: "usage error", code: UsageErrorCode} + ErrInternal = ErrorCode{msg: "internal error", code: InternalErrorCode} + ErrUnimplemented = ErrorCode{msg: "unimplemented", code: UnimplementedErrorCode} +) diff --git a/src/go/arcadium/entropylex/cmd/entropylex/error_code_test.go b/src/go/arcadium/entropylex/cmd/entropylex/error_code_test.go new file mode 100644 index 0000000..68af67c --- /dev/null +++ b/src/go/arcadium/entropylex/cmd/entropylex/error_code_test.go @@ -0,0 +1,48 @@ +package main_test + +import ( + "testing" + + el "github.com/AlphaPixel/EntropyLex/src/go/arcadium/entropylex/cmd/entropylex" + "github.com/AlphaPixel/EntropyLex/src/go/arcadium/test/assert" +) + +func Test_ErrorCode(t *testing.T) { + + tests := []struct { + name string + err el.ErrorCode + verify func(t *testing.T, err el.ErrorCode) + }{ + { + name: "ErrUsage", + err: el.ErrUsage, + verify: func(t *testing.T, err el.ErrorCode) { + assert.Error(t, err, "usage error") + assert.Equal(t, err.Code(), el.UsageErrorCode) + }, + }, + { + name: "ErrInternal", + err: el.ErrInternal, + verify: func(t *testing.T, err el.ErrorCode) { + assert.Error(t, err, "internal error") + assert.Equal(t, err.Code(), el.InternalErrorCode) + }, + }, + { + name: "ErrUnimplemented", + err: el.ErrUnimplemented, + verify: func(t *testing.T, err el.ErrorCode) { + assert.Error(t, err, "unimplemented") + assert.Equal(t, err.Code(), el.UnimplementedErrorCode) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.verify(t, test.err) + }) + } +} diff --git a/src/go/arcadium/entropylex/cmd/entropylex/inputfile.go b/src/go/arcadium/entropylex/cmd/entropylex/inputfile.go new file mode 100644 index 0000000..a1ad15d --- /dev/null +++ b/src/go/arcadium/entropylex/cmd/entropylex/inputfile.go @@ -0,0 +1,52 @@ +// MIT License +// +// Copyright 2026 arcadium.dev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package main + +import ( + "errors" + "fmt" + "os" +) + +// InputFile opens the input file for reading given the input filename. +func InputFile(filename string) (*os.File, error) { + if filename == "" || filename == "-" { + return nil, nil + } + + fs, err := os.Stat(filename) + if err != nil { + return nil, err + } + mode := fs.Mode() + + if !mode.IsRegular() { + errmsg := fmt.Sprintf("input file \"%s\" is not a regular file", filename) + if mode.IsDir() { + errmsg = fmt.Sprintf("input file \"%s\" is a directory", filename) + } + return nil, errors.New(errmsg) + } + + return os.Open(filename) +} diff --git a/src/go/arcadium/entropylex/cmd/entropylex/inputfile_test.go b/src/go/arcadium/entropylex/cmd/entropylex/inputfile_test.go new file mode 100644 index 0000000..c0074b9 --- /dev/null +++ b/src/go/arcadium/entropylex/cmd/entropylex/inputfile_test.go @@ -0,0 +1,95 @@ +package main_test + +import ( + "os" + "testing" + + el "github.com/AlphaPixel/EntropyLex/src/go/arcadium/entropylex/cmd/entropylex" + "github.com/AlphaPixel/EntropyLex/src/go/arcadium/test/assert" +) + +func Test_InputFile(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + filename string + verify func(*testing.T, *os.File, error) + }{ + { + name: "empty filename", + filename: "", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, err) + assert.Nil(t, f) + }, + }, + { + name: "- filename", + filename: "-", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, err) + assert.Nil(t, f) + }, + }, + { + name: "unknown filename", + filename: "xyz.pdq", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, "stat xyz.pdq: no such file or directory") + }, + }, + { + name: "directory", + filename: "test", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, `input file "test" is a directory`) + }, + }, + { + name: "link to directory", + filename: "./test/dir_link", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, `input file "./test/dir_link" is a directory`) + }, + }, + { + name: "bad link", + filename: "./test/bad_link", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, "stat ./test/bad_link: no such file or directory") + }, + }, + { + name: "good file", + filename: "main.go", + verify: func(t *testing.T, f *os.File, err error) { + assert.NotNil(t, f) + assert.Nil(t, err) + assert.Nil(t, f.Close()) + }, + }, + { + name: "link to good file", + filename: "test/good_link", + verify: func(t *testing.T, f *os.File, err error) { + assert.NotNil(t, f) + assert.Nil(t, err) + assert.Nil(t, f.Close()) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + f, err := el.InputFile(test.filename) + test.verify(t, f, err) + }) + } +} diff --git a/src/go/arcadium/entropylex/cmd/entropylex/main.go b/src/go/arcadium/entropylex/cmd/entropylex/main.go index 8f6dbd0..9dfd7a8 100644 --- a/src/go/arcadium/entropylex/cmd/entropylex/main.go +++ b/src/go/arcadium/entropylex/cmd/entropylex/main.go @@ -24,6 +24,8 @@ package main import ( "context" + "errors" + "fmt" "os" "path/filepath" @@ -50,6 +52,11 @@ func Main() error { func main() { if err := Main(); err != nil { - os.Exit(1) + fmt.Fprintf(os.Stderr, "%v\n", err) + var errorCode ErrorCode + if errors.As(err, &errorCode) { + os.Exit(errorCode.Code()) + } + os.Exit(UnknownErrorCode) } } diff --git a/src/go/arcadium/entropylex/cmd/entropylex/outputfile.go b/src/go/arcadium/entropylex/cmd/entropylex/outputfile.go new file mode 100644 index 0000000..7db7bd8 --- /dev/null +++ b/src/go/arcadium/entropylex/cmd/entropylex/outputfile.go @@ -0,0 +1,64 @@ +// MIT License +// +// Copyright 2026 arcadium.dev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package main + +import ( + "errors" + "fmt" + "os" +) + +// OutputFile creates an output file give the output filename and the force flag. +func OutputFile(filename string, force bool) (*os.File, error) { + if filename == "" { + return nil, nil + } + + // See if the file exists. If not Create it. + fs, err := os.Stat(filename) + if err != nil { + return os.Create(filename) + } + mode := fs.Mode() + + // If the file exists and it isn't a regular file, return an error. + if !mode.IsRegular() { + errmsg := fmt.Sprintf("output file \"%s\" is not a regular file", filename) + if mode.IsDir() { + errmsg = fmt.Sprintf("output file \"%s\" is a directory", filename) + } + return nil, errors.New(errmsg) + } + + // If the file exists and it's a regular file, create it if the size is 0. + if fs.Size() == 0 { + return os.Create(filename) + } + + // If the size is non-zero and the force flag isn't present, return an error. + if !force { + return nil, errors.New("a non-empty output file exists, to overwrite use the --force option") + } + + return os.Create(filename) +} diff --git a/src/go/arcadium/entropylex/cmd/entropylex/outputfile_test.go b/src/go/arcadium/entropylex/cmd/entropylex/outputfile_test.go new file mode 100644 index 0000000..2b057f8 --- /dev/null +++ b/src/go/arcadium/entropylex/cmd/entropylex/outputfile_test.go @@ -0,0 +1,141 @@ +package main_test + +import ( + "fmt" + "os" + "testing" + "uuid" + + el "github.com/AlphaPixel/EntropyLex/src/go/arcadium/entropylex/cmd/entropylex" + "github.com/AlphaPixel/EntropyLex/src/go/arcadium/test/assert" +) + +func Test_OutputFile(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + filename string + force bool + before func(*testing.T, string) + verify func(*testing.T, *os.File, error) + after func(*testing.T, string) + }{ + { + name: "empty filename", + filename: "", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, err) + assert.Nil(t, f) + }, + }, + { + name: "file does not exist", + filename: fmt.Sprintf("test/%s", uuid.New().String()), + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, err) + assert.NotNil(t, f) + assert.Nil(t, f.Close()) + }, + after: func(t *testing.T, output string) { + assert.Nil(t, os.Remove(output)) + }, + }, + { + name: "file does not exist, it non-existant path", + filename: "test/foo/bar/this_should_fail", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, "open test/foo/bar/this_should_fail: no such file or directory") + }, + }, + { + name: "file exists, directory", + filename: "test", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, `output file "test" is a directory`) + }, + }, + { + name: "file exists, bad link", + filename: "test/bad_link", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, "open test/bad_link: no such file or directory") + }, + }, + { + name: "file exists, empty", + filename: "test/output_empty", + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, err) + assert.NotNil(t, f) + fs, e := f.Stat() + assert.Nil(t, e) + assert.Equal(t, fs.Size(), 0) + }, + }, + { + name: "file exists, not empty w/o force", + filename: fmt.Sprintf("test/%s", uuid.New().String()), + force: false, + before: func(t *testing.T, output string) { + f, err := os.Create(output) + assert.NotNil(t, f) + assert.Nil(t, err) + _, err = f.Write([]byte("testing 1 2 3 4")) + assert.Nil(t, err) + assert.Nil(t, f.Close()) + }, + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, f) + assert.Error(t, err, "a non-empty output file exists, to overwrite use the --force option") + }, + after: func(t *testing.T, output string) { + assert.Nil(t, os.Remove(output)) + }, + }, + { + name: "file exists, not empty w/force", + filename: fmt.Sprintf("test/%s", uuid.New().String()), + force: true, + before: func(t *testing.T, output string) { + f, err := os.Create(output) + assert.NotNil(t, f) + assert.Nil(t, err) + _, err = f.Write([]byte("testing 1 2 3 4")) + assert.Nil(t, err) + assert.Nil(t, f.Close()) + }, + verify: func(t *testing.T, f *os.File, err error) { + assert.Nil(t, err) + assert.NotNil(t, f) + fs, e := f.Stat() + assert.Nil(t, e) + assert.Equal(t, fs.Size(), 0) + assert.Nil(t, f.Close()) + }, + after: func(t *testing.T, output string) { + assert.Nil(t, os.Remove(output)) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + if test.before != nil { + test.before(t, test.filename) + } + + f, err := el.OutputFile(test.filename, test.force) + test.verify(t, f, err) + + if test.after != nil { + test.after(t, test.filename) + } + }) + } +} diff --git a/src/go/arcadium/go.mod b/src/go/arcadium/go.mod index 9eda53f..b665cb6 100644 --- a/src/go/arcadium/go.mod +++ b/src/go/arcadium/go.mod @@ -1,6 +1,6 @@ module github.com/AlphaPixel/EntropyLex/src/go/arcadium -go 1.26 +go 1.27 tool ( golang.org/x/vuln/cmd/govulncheck @@ -9,7 +9,6 @@ tool ( require ( github.com/google/go-cmp v0.7.0 - github.com/google/uuid v1.6.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 github.com/urfave/cli/v3 v3.11.0 ) @@ -24,5 +23,5 @@ require ( golang.org/x/text v0.39.0 // indirect golang.org/x/tools v0.49.0 // indirect golang.org/x/vuln v1.7.0 // indirect - honnef.co/go/tools v0.7.0 // indirect + honnef.co/go/tools v0.8.1 // indirect ) diff --git a/src/go/arcadium/go.sum b/src/go/arcadium/go.sum index d976b34..f488fba 100644 --- a/src/go/arcadium/go.sum +++ b/src/go/arcadium/go.sum @@ -10,8 +10,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= @@ -42,5 +40,5 @@ golang.org/x/vuln v1.7.0 h1:4MQBuhmXbz2uepNJrf3v+aaZLGDqw1JluwYboegA1qg= golang.org/x/vuln v1.7.0/go.mod h1:Xw7zvU3e1bsCYYBXu+w4wcn2Kgn27f34WBCTw8LL5Us= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= -honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= +honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w= +honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks= diff --git a/src/go/arcadium/unicode/code_point.go b/src/go/arcadium/unicode/code_point.go index 6af164b..604027b 100644 --- a/src/go/arcadium/unicode/code_point.go +++ b/src/go/arcadium/unicode/code_point.go @@ -49,11 +49,11 @@ func (cp CodePoint) Decode() (string, error) { s := string(cp) if s == "" { - return "", fmt.Errorf("%w: \"\"", ErrInvalidCodePoint) + return "", fmt.Errorf("%w, \"\"", ErrInvalidCodePoint) } if !reCodePoint.MatchString(s) { - return "", fmt.Errorf("%w: %s", ErrInvalidCodePoint, s) + return "", fmt.Errorf("%w, %s", ErrInvalidCodePoint, s) } // Yes, I know I am ignoring the error from the parse. Yes, I know this is a @@ -63,9 +63,9 @@ func (cp CodePoint) Decode() (string, error) { switch { case i > maxCodePoint: - return "", fmt.Errorf("%w: %s", ErrInvalidCodePoint, s) + return "", fmt.Errorf("%w, %s", ErrInvalidCodePoint, s) case i >= surrogateRangeLow && i <= surrogateRangeHi: - return "", fmt.Errorf("%w: surrogate code point %s", ErrInvalidCodePoint, s) + return "", fmt.Errorf("%w, surrogate code point %s", ErrInvalidCodePoint, s) } return string(rune(i)), nil } diff --git a/src/go/arcadium/unicode/code_point_test.go b/src/go/arcadium/unicode/code_point_test.go index 2bd8af3..c917026 100644 --- a/src/go/arcadium/unicode/code_point_test.go +++ b/src/go/arcadium/unicode/code_point_test.go @@ -22,7 +22,7 @@ func Test_UnicodeCodePoint_String(t *testing.T) { verify: func(t *testing.T, s string, err error) { assert.Equal(t, s, "") assert.IsError(t, err, unicode.ErrInvalidCodePoint) - assert.Error(t, err, `invalid unicode code point: ""`) + assert.Error(t, err, `invalid unicode code point, ""`) }, }, { @@ -31,7 +31,7 @@ func Test_UnicodeCodePoint_String(t *testing.T) { verify: func(t *testing.T, s string, err error) { assert.Equal(t, s, "") assert.IsError(t, err, unicode.ErrInvalidCodePoint) - assert.Error(t, err, `invalid unicode code point: U+00020`) + assert.Error(t, err, `invalid unicode code point, U+00020`) }, }, { @@ -40,7 +40,7 @@ func Test_UnicodeCodePoint_String(t *testing.T) { verify: func(t *testing.T, s string, err error) { assert.Equal(t, s, "") assert.IsError(t, err, unicode.ErrInvalidCodePoint) - assert.Error(t, err, "invalid unicode code point: foobar") + assert.Error(t, err, "invalid unicode code point, foobar") }, }, { @@ -49,7 +49,7 @@ func Test_UnicodeCodePoint_String(t *testing.T) { verify: func(t *testing.T, s string, err error) { assert.Equal(t, s, "") assert.IsError(t, err, unicode.ErrInvalidCodePoint) - assert.Error(t, err, "invalid unicode code point: surrogate code point U+D800") + assert.Error(t, err, "invalid unicode code point, surrogate code point U+D800") }, }, { @@ -58,7 +58,7 @@ func Test_UnicodeCodePoint_String(t *testing.T) { verify: func(t *testing.T, s string, err error) { assert.Equal(t, s, "") assert.IsError(t, err, unicode.ErrInvalidCodePoint) - assert.Error(t, err, "invalid unicode code point: surrogate code point U+DFFF") + assert.Error(t, err, "invalid unicode code point, surrogate code point U+DFFF") }, }, { @@ -67,7 +67,7 @@ func Test_UnicodeCodePoint_String(t *testing.T) { verify: func(t *testing.T, s string, err error) { assert.Equal(t, s, "") assert.IsError(t, err, unicode.ErrInvalidCodePoint) - assert.Error(t, err, "invalid unicode code point: surrogate code point U+DCDE") + assert.Error(t, err, "invalid unicode code point, surrogate code point U+DCDE") }, }, { @@ -76,7 +76,7 @@ func Test_UnicodeCodePoint_String(t *testing.T) { verify: func(t *testing.T, s string, err error) { assert.Equal(t, s, "") assert.IsError(t, err, unicode.ErrInvalidCodePoint) - assert.Error(t, err, "invalid unicode code point: U+110000") + assert.Error(t, err, "invalid unicode code point, U+110000") }, }, // success