diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 15973cf..39ec15a 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -301,7 +301,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr case 1: // instance name := args[0] - config, entry := tryShort(msg, name) + config, entry := tryIdentifier(msg, name) if config == nil { config = tryPath(msg, name) } diff --git a/cmd/hakurei/parse.go b/cmd/hakurei/parse.go index 42eaf73..4e0d2d7 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -1,6 +1,7 @@ package main import ( + "encoding/hex" "errors" "io" "log" @@ -15,6 +16,9 @@ import ( "hakurei.app/message" ) +// tryPath attempts to read [hst.Config] from multiple sources. +// tryPath reads from [os.Stdin] if name has value "-". +// Otherwise, name is passed to tryFd, and if that returns nil, name is passed to [os.Open]. func tryPath(msg message.Msg, name string) (config *hst.Config) { var r io.ReadCloser config = new(hst.Config) @@ -42,6 +46,7 @@ func tryPath(msg message.Msg, name string) (config *hst.Config) { return } +// tryFd returns a [io.ReadCloser] if name represents an integer corresponding to a valid file descriptor. func tryFd(msg message.Msg, name string) io.ReadCloser { if v, err := strconv.Atoi(name); err != nil { if !errors.Is(err, strconv.ErrSyntax) { @@ -61,10 +66,48 @@ func tryFd(msg message.Msg, name string) io.ReadCloser { } } -func tryShort(msg message.Msg, name string) (config *hst.Config, entry *hst.State) { - likePrefix := false - if len(name) <= 32 { - likePrefix = true +// shortLengthMin is the minimum length a short form identifier can have and still be interpreted as an identifier. +const shortLengthMin = 1 << 3 + +// shortIdentifier returns an eight character short representation of [hst.ID] from its random bytes. +func shortIdentifier(id *hst.ID) string { + return shortIdentifierString(id.String()) +} + +// shortIdentifierString implements shortIdentifier on an arbitrary string. +func shortIdentifierString(s string) string { + return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin] +} + +// tryIdentifier attempts to match [hst.State] from a [hex] representation of [hst.ID] or a prefix of its lower half. +func tryIdentifier(msg message.Msg, name string) (config *hst.Config, entry *hst.State) { + return tryIdentifierEntries(msg, name, func() map[hst.ID]*hst.State { + var sc hst.Paths + app.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil)) + s := state.NewMulti(msg, sc.RunDirPath.String()) + if entries, err := state.Join(s); err != nil { + msg.GetLogger().Printf("cannot join store: %v", err) // not fatal + return nil + } else { + return entries + } + }) +} + +// tryIdentifierEntries implements tryIdentifier with a custom entries pair getter. +func tryIdentifierEntries( + msg message.Msg, + name string, + getEntries func() map[hst.ID]*hst.State, +) (config *hst.Config, entry *hst.State) { + const ( + likeShort = 1 << iota + likeFull + ) + + var likely uintptr + if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) { // half the hex representation + // cannot safely decode here due to unknown alignment for _, c := range name { if c >= '0' && c <= '9' { continue @@ -72,35 +115,50 @@ func tryShort(msg message.Msg, name string) (config *hst.Config, entry *hst.Stat if c >= 'a' && c <= 'f' { continue } - likePrefix = false - break + return } + likely |= likeShort + } else if len(name) == hex.EncodedLen(len(hst.ID{})) { + likely |= likeFull } - // try to match from state store - if likePrefix && len(name) >= 8 { - msg.Verbose("argument looks like prefix") + if likely == 0 { + return + } + entries := getEntries() + if entries == nil { + return + } - var sc hst.Paths - app.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil)) - s := state.NewMulti(msg, sc.RunDirPath.String()) - if entries, err := state.Join(s); err != nil { - log.Printf("cannot join store: %v", err) - // drop to fetch from file - } else { - for id := range entries { - v := id.String() - if strings.HasPrefix(v, name) { - // match, use config from this state entry - entry = entries[id] - config = entry.Config - break - } - - msg.Verbosef("instance %s skipped", v) + switch { + case likely&likeShort != 0: + msg.Verbose("argument looks like short identifier") + for id := range entries { + v := id.String() + if strings.HasPrefix(v[len(hst.ID{}):], name) { + // match, use config from this state entry + entry = entries[id] + config = entry.Config + break } - } - } - return + msg.Verbosef("instance %s skipped", v) + } + return + + case likely&likeFull != 0: + var likelyID hst.ID + if likelyID.UnmarshalText([]byte(name)) != nil { + return + } + msg.Verbose("argument looks like identifier") + if ent, ok := entries[likelyID]; ok { + entry = ent + config = ent.Config + } + return + + default: + panic("unreachable") + } } diff --git a/cmd/hakurei/parse_test.go b/cmd/hakurei/parse_test.go new file mode 100644 index 0000000..d8a0ff6 --- /dev/null +++ b/cmd/hakurei/parse_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "reflect" + "testing" + + "hakurei.app/hst" + "hakurei.app/message" +) + +func TestShortIdentifier(t *testing.T) { + t.Parallel() + id := hst.ID{ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, + } + + const want = "fedcba98" + if got := shortIdentifier(&id); got != want { + t.Errorf("shortIdentifier: %q, want %q", got, want) + } +} + +func TestTryIdentifier(t *testing.T) { + t.Parallel() + msg := message.NewMsg(nil) + id := hst.ID{ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, + } + + testCases := []struct { + name string + s string + entries map[hst.ID]*hst.State + want *hst.State + }{ + {"likely entries fault", "ffffffff", nil, nil}, + + {"likely short too short", "ff", nil, nil}, + {"likely short too long", "fffffffffffffffff", nil, nil}, + {"likely short invalid lower", "fffffff\x00", nil, nil}, + {"likely short invalid higher", "0000000\xff", nil, nil}, + {"short no match", "fedcba98", map[hst.ID]*hst.State{hst.ID{}: nil}, nil}, + {"short match", "fedcba98", map[hst.ID]*hst.State{ + hst.ID{}: nil, + id: { + ID: id, + PID: 0xcafebabe, + ShimPID: 0xdeadbeef, + Config: hst.Template(), + }, + }, &hst.State{ + ID: id, + PID: 0xcafebabe, + ShimPID: 0xdeadbeef, + Config: hst.Template(), + }}, + {"short match longer", "fedcba98765", map[hst.ID]*hst.State{ + hst.ID{}: nil, + id: { + ID: id, + PID: 0xcafebabe, + ShimPID: 0xdeadbeef, + Config: hst.Template(), + }, + }, &hst.State{ + ID: id, + PID: 0xcafebabe, + ShimPID: 0xdeadbeef, + Config: hst.Template(), + }}, + + {"likely long invalid", "0123456789abcdeffedcba987654321\x00", map[hst.ID]*hst.State{}, nil}, + {"long no match", "0123456789abcdeffedcba9876543210", map[hst.ID]*hst.State{hst.ID{}: nil}, nil}, + {"long match", "0123456789abcdeffedcba9876543210", map[hst.ID]*hst.State{ + hst.ID{}: nil, + id: { + ID: id, + PID: 0xcafebabe, + ShimPID: 0xdeadbeef, + Config: hst.Template(), + }, + }, &hst.State{ + ID: id, + PID: 0xcafebabe, + ShimPID: 0xdeadbeef, + Config: hst.Template(), + }}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, got := tryIdentifierEntries(msg, tc.s, func() map[hst.ID]*hst.State { return tc.entries }) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("tryIdentifier: %#v, want %#v", got, tc.want) + } + }) + } +} diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index b1a0c86..3b4730f 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -215,7 +215,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo encodeJSON(log.Fatal, output, short, v) } else { for _, e := range exp { - mustPrintln(output, e.s[:8]) + mustPrintln(output, shortIdentifierString(e.s)) } } return @@ -237,12 +237,12 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo as = strconv.Itoa(e.Config.Identity) id := e.Config.ID if id == "" { - id = "app.hakurei." + e.s[:8] + id = "app.hakurei." + shortIdentifierString(e.s) } as += " (" + id + ")" } t.Printf("\t%s\t%d\t%s\t%s\n", - e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String()) + shortIdentifierString(e.s), e.PID, as, now.Sub(e.Time).Round(time.Second).String()) } } diff --git a/cmd/hakurei/print_test.go b/cmd/hakurei/print_test.go index 41ce554..343073a 100644 --- a/cmd/hakurei/print_test.go +++ b/cmd/hakurei/print_test.go @@ -523,13 +523,13 @@ func TestPrintPs(t *testing.T) { {"state corruption", map[hst.ID]*hst.State{hst.ID{}: testState}, false, false, " Instance PID Application Uptime\n"}, {"valid pd", map[hst.ID]*hst.State{testID: {ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime - 8e2c76b0 256 0 (app.hakurei.8e2c76b0) 1h2m32s + 4cf073bd 256 0 (app.hakurei.4cf073bd) 1h2m32s `}, {"valid", map[hst.ID]*hst.State{testID: testState}, false, false, ` Instance PID Application Uptime - 8e2c76b0 3405691582 9 (org.chromium.Chromium) 1h2m32s + 4cf073bd 3405691582 9 (org.chromium.Chromium) 1h2m32s `}, - {"valid short", map[hst.ID]*hst.State{testID: testState}, true, false, "8e2c76b0\n"}, + {"valid short", map[hst.ID]*hst.State{testID: testState}, true, false, "4cf073bd\n"}, {"valid json", map[hst.ID]*hst.State{testID: testState}, false, true, `{ "8e2c76b066dabe574cf073bdb46eb5c1": { "instance": "8e2c76b066dabe574cf073bdb46eb5c1",