From 823575acaccfb34d908dad2ae14551fa425d7431 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 16 Apr 2026 16:00:32 +0900 Subject: [PATCH] cmd/mbf: move info command This is cleaner with less shared state. Signed-off-by: Ophestra --- cmd/mbf/info.go | 127 ++++++++++++++++++++++++++++++++ cmd/mbf/info_test.go | 170 +++++++++++++++++++++++++++++++++++++++++++ cmd/mbf/main.go | 95 +----------------------- 3 files changed, 298 insertions(+), 94 deletions(-) create mode 100644 cmd/mbf/info.go create mode 100644 cmd/mbf/info_test.go diff --git a/cmd/mbf/info.go b/cmd/mbf/info.go new file mode 100644 index 00000000..f421ce66 --- /dev/null +++ b/cmd/mbf/info.go @@ -0,0 +1,127 @@ +package main + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "hakurei.app/internal/pkg" + "hakurei.app/internal/rosa" +) + +// commandInfo implements the info subcommand. +func commandInfo( + cm *cache, + args []string, + w io.Writer, + writeStatus bool, + reportPath string, +) (err error) { + if len(args) == 0 { + return errors.New("info requires at least 1 argument") + } + + var r *rosa.Report + if reportPath != "" { + if r, err = rosa.OpenReport(reportPath); err != nil { + return err + } + defer func() { + if closeErr := r.Close(); err == nil { + err = closeErr + } + }() + defer r.HandleAccess(&err)() + } + + // recovered by HandleAccess + mustPrintln := func(a ...any) { + if _, _err := fmt.Fprintln(w, a...); _err != nil { + panic(_err) + } + } + mustPrint := func(a ...any) { + if _, _err := fmt.Fprint(w, a...); _err != nil { + panic(_err) + } + } + + for i, name := range args { + if p, ok := rosa.ResolveName(name); !ok { + return fmt.Errorf("unknown artifact %q", name) + } else { + var suffix string + if version := rosa.Std.Version(p); version != rosa.Unversioned { + suffix += "-" + version + } + mustPrintln("name : " + name + suffix) + + meta := rosa.GetMetadata(p) + mustPrintln("description : " + meta.Description) + if meta.Website != "" { + mustPrintln("website : " + + strings.TrimSuffix(meta.Website, "/")) + } + if len(meta.Dependencies) > 0 { + mustPrint("depends on :") + for _, d := range meta.Dependencies { + s := rosa.GetMetadata(d).Name + if version := rosa.Std.Version(d); version != rosa.Unversioned { + s += "-" + version + } + mustPrint(" " + s) + } + mustPrintln() + } + + const statusPrefix = "status : " + if writeStatus { + if r == nil { + var f io.ReadSeekCloser + err = cm.Do(func(cache *pkg.Cache) (err error) { + f, err = cache.OpenStatus(rosa.Std.Load(p)) + return + }) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + mustPrintln( + statusPrefix + "not yet cured", + ) + } else { + return + } + } else { + mustPrint(statusPrefix) + _, err = io.Copy(w, f) + if err = errors.Join(err, f.Close()); err != nil { + return + } + } + } else if err = cm.Do(func(cache *pkg.Cache) (err error) { + status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p))) + if status == nil { + mustPrintln( + statusPrefix + "not in report", + ) + } else { + mustPrintln("size :", n) + mustPrint(statusPrefix) + if _, err = w.Write(status); err != nil { + return + } + } + return + }); err != nil { + return + } + } + + if i != len(args)-1 { + mustPrintln() + } + } + } + return nil +} diff --git a/cmd/mbf/info_test.go b/cmd/mbf/info_test.go new file mode 100644 index 00000000..16e94364 --- /dev/null +++ b/cmd/mbf/info_test.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "reflect" + "strings" + "syscall" + "testing" + "unsafe" + + "hakurei.app/internal/pkg" + "hakurei.app/internal/rosa" + "hakurei.app/message" +) + +func TestInfo(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + args []string + status map[string]string + report string + want string + wantErr any + }{ + {"qemu", []string{"qemu"}, nil, "", ` +name : qemu-` + rosa.Std.Version(rosa.QEMU) + ` +description : a generic and open source machine emulator and virtualizer +website : https://www.qemu.org +depends on : glib-` + rosa.Std.Version(rosa.GLib) + ` zstd-` + rosa.Std.Version(rosa.Zstd) + ` +`, nil}, + + {"multi", []string{"hakurei", "hakurei-dist"}, nil, "", ` +name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + ` +description : low-level userspace tooling for Rosa OS +website : https://hakurei.app + +name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + ` +description : low-level userspace tooling for Rosa OS (distribution tarball) +website : https://hakurei.app +`, nil}, + + {"nonexistent", []string{"zlib", "\x00"}, nil, "", ` +name : zlib-` + rosa.Std.Version(rosa.Zlib) + ` +description : lossless data-compression library +website : https://zlib.net + +`, fmt.Errorf("unknown artifact %q", "\x00")}, + + {"status cache", []string{"zlib", "zstd"}, map[string]string{ + "zstd": "internal/pkg (amd64) on satori\n", + "hakurei": "internal/pkg (amd64) on satori\n\n", + }, "", ` +name : zlib-` + rosa.Std.Version(rosa.Zlib) + ` +description : lossless data-compression library +website : https://zlib.net +status : not yet cured + +name : zstd-` + rosa.Std.Version(rosa.Zstd) + ` +description : a fast compression algorithm +website : https://facebook.github.io/zstd +status : internal/pkg (amd64) on satori +`, nil}, + + {"status cache perm", []string{"zlib"}, map[string]string{ + "zlib": "\x00", + }, "", ` +name : zlib-` + rosa.Std.Version(rosa.Zlib) + ` +description : lossless data-compression library +website : https://zlib.net +`, func(cm *cache) error { + return &os.PathError{ + Op: "open", + Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(rosa.Std.Load(rosa.Zlib)).Value())), + Err: syscall.EACCES, + } + }}, + + {"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), ` +name : zlib-` + rosa.Std.Version(rosa.Zlib) + ` +description : lossless data-compression library +website : https://zlib.net +status : not in report +`, nil}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var ( + cm *cache + buf strings.Builder + rp string + ) + + if tc.status != nil || tc.report != "" { + cm = &cache{ + ctx: context.Background(), + msg: message.New(log.New(os.Stderr, "info: ", 0)), + base: t.TempDir(), + } + defer cm.Close() + } + + if tc.report != "" { + rp = filepath.Join(t.TempDir(), "report") + if err := os.WriteFile( + rp, + unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)), + 0400, + ); err != nil { + t.Fatal(err) + } + } + + if tc.status != nil { + for name, status := range tc.status { + p, ok := rosa.ResolveName(name) + if !ok { + t.Fatalf("invalid name %q", name) + } + perm := os.FileMode(0400) + if status == "\x00" { + perm = 0 + } + if err := cm.Do(func(cache *pkg.Cache) error { + return os.WriteFile(filepath.Join( + cm.base, + "status", + pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()), + ), unsafe.Slice(unsafe.StringData(status), len(status)), perm) + }); err != nil { + t.Fatalf("Do: error = %v", err) + } + } + } + + var wantErr error + switch c := tc.wantErr.(type) { + case error: + wantErr = c + case func(cm *cache) error: + wantErr = c(cm) + default: + if tc.wantErr != nil { + t.Fatalf("invalid wantErr %#v", tc.wantErr) + } + } + + if err := commandInfo( + cm, + tc.args, + &buf, + cm != nil, + rp, + ); !reflect.DeepEqual(err, wantErr) { + t.Fatalf("commandInfo: error = %v, want %v", err, wantErr) + } + + if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") { + t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want) + } + }) + } +} diff --git a/cmd/mbf/main.go b/cmd/mbf/main.go index bfa2aa07..675f8bb2 100644 --- a/cmd/mbf/main.go +++ b/cmd/mbf/main.go @@ -24,7 +24,6 @@ import ( "path/filepath" "runtime" "strconv" - "strings" "sync" "sync/atomic" "syscall" @@ -149,99 +148,7 @@ func main() { "info", "Display out-of-band metadata of an artifact", func(args []string) (err error) { - if len(args) == 0 { - return errors.New("info requires at least 1 argument") - } - - var r *rosa.Report - if flagReport != "" { - if r, err = rosa.OpenReport(flagReport); err != nil { - return err - } - defer func() { - if closeErr := r.Close(); err == nil { - err = closeErr - } - }() - defer r.HandleAccess(&err)() - } - - for i, name := range args { - if p, ok := rosa.ResolveName(name); !ok { - return fmt.Errorf("unknown artifact %q", name) - } else { - var suffix string - if version := rosa.Std.Version(p); version != rosa.Unversioned { - suffix += "-" + version - } - fmt.Println("name : " + name + suffix) - - meta := rosa.GetMetadata(p) - fmt.Println("description : " + meta.Description) - if meta.Website != "" { - fmt.Println("website : " + - strings.TrimSuffix(meta.Website, "/")) - } - if len(meta.Dependencies) > 0 { - fmt.Print("depends on :") - for _, d := range meta.Dependencies { - s := rosa.GetMetadata(d).Name - if version := rosa.Std.Version(d); version != rosa.Unversioned { - s += "-" + version - } - fmt.Print(" " + s) - } - fmt.Println() - } - - const statusPrefix = "status : " - if flagStatus { - if r == nil { - var f io.ReadSeekCloser - err = cm.Do(func(cache *pkg.Cache) (err error) { - f, err = cache.OpenStatus(rosa.Std.Load(p)) - return - }) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - fmt.Println( - statusPrefix + "not yet cured", - ) - } else { - return - } - } else { - fmt.Print(statusPrefix) - _, err = io.Copy(os.Stdout, f) - if err = errors.Join(err, f.Close()); err != nil { - return - } - } - } else if err = cm.Do(func(cache *pkg.Cache) (err error) { - status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p))) - if status == nil { - fmt.Println( - statusPrefix + "not in report", - ) - } else { - fmt.Println("size :", n) - fmt.Print(statusPrefix) - if _, err = os.Stdout.Write(status); err != nil { - return - } - } - return - }); err != nil { - return - } - } - - if i != len(args)-1 { - fmt.Println() - } - } - } - return nil + return commandInfo(&cm, args, os.Stdout, flagStatus, flagReport) }, ). Flag(