diff --git a/ldd/error.go b/ldd/error.go deleted file mode 100644 index 39b63ef..0000000 --- a/ldd/error.go +++ /dev/null @@ -1,27 +0,0 @@ -package ldd - -import ( - "errors" - "fmt" -) - -var ( - ErrUnexpectedSeparator = errors.New("unexpected separator") - ErrPathNotAbsolute = errors.New("path not absolute") - ErrBadLocationFormat = errors.New("bad location format") - ErrUnexpectedNewline = errors.New("unexpected newline") -) - -type EntryUnexpectedSegmentsError string - -func (e EntryUnexpectedSegmentsError) Is(err error) bool { - var eq EntryUnexpectedSegmentsError - if !errors.As(err, &eq) { - return false - } - return e == eq -} - -func (e EntryUnexpectedSegmentsError) Error() string { - return fmt.Sprintf("unexpected segments in entry %q", string(e)) -} diff --git a/ldd/exec.go b/ldd/exec.go index c52eff1..d91cd65 100644 --- a/ldd/exec.go +++ b/ldd/exec.go @@ -3,6 +3,7 @@ package ldd import ( "bytes" "context" + "errors" "io" "os" "os/exec" @@ -16,11 +17,14 @@ import ( "hakurei.app/message" ) -var ( - msgStatic = []byte("Not a valid dynamic program") - msgStaticGlibc = []byte("not a dynamic executable") +const ( + // msgStaticSuffix is the suffix of message printed to stderr by musl on a statically linked program. + msgStaticSuffix = ": Not a valid dynamic program" + // msgStaticGlibc is a substring of the message printed to stderr by glibc on a statically linked program. + msgStaticGlibc = "not a dynamic executable" ) +// Exec runs ldd(1) in a restrictive [container] and connects it to a [Decoder], returning resulting entries. func Exec(ctx context.Context, msg message.Msg, p string) ([]*Entry, error) { const ( lddName = "ldd" @@ -41,14 +45,20 @@ func Exec(ctx context.Context, msg message.Msg, p string) ([]*Entry, error) { z.Hostname = "hakurei-" + lddName z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompPresets |= std.PresetStrict - stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) - z.Stdout = stdout + stderr := new(bytes.Buffer) z.Stderr = stderr z. Bind(fhs.AbsRoot, fhs.AbsRoot, 0). Proc(fhs.AbsProc). Dev(fhs.AbsDev, false) + var d *Decoder + if r, err := z.StdoutPipe(); err != nil { + return nil, err + } else { + d = NewDecoder(r) + } + if err := z.Start(); err != nil { return nil, err } @@ -56,15 +66,18 @@ func Exec(ctx context.Context, msg message.Msg, p string) ([]*Entry, error) { if err := z.Serve(); err != nil { return nil, err } + + entries, decodeErr := d.Decode() if err := z.Wait(); err != nil { m := stderr.Bytes() - if bytes.Contains(m, append([]byte(p+": "), msgStatic...)) || - bytes.Contains(m, msgStaticGlibc) { + if bytes.Contains(m, []byte(msgStaticSuffix)) || bytes.Contains(m, []byte(msgStaticGlibc)) { return nil, nil } + + if decodeErr != nil { + return nil, errors.Join(decodeErr, err) + } return nil, err } - - v := stdout.Bytes() - return Parse(v) + return entries, decodeErr } diff --git a/ldd/ldd.go b/ldd/ldd.go index ca4156c..307d848 100644 --- a/ldd/ldd.go +++ b/ldd/ldd.go @@ -2,65 +2,203 @@ package ldd import ( - "math" - "path" + "bufio" + "bytes" + "errors" + "fmt" + "io" "strconv" - "strings" + + "hakurei.app/container/check" ) +var ( + // ErrUnexpectedNewline is returned when encountering an unexpected empty line. + ErrUnexpectedNewline = errors.New("unexpected newline") + // ErrUnexpectedSeparator is returned when encountering an unexpected separator segment. + ErrUnexpectedSeparator = errors.New("unexpected separator") + // ErrBadLocationFormat is returned for an incorrectly formatted [Entry.Location] segment. + ErrBadLocationFormat = errors.New("bad location format") +) + +// EntryUnexpectedSegmentsError is returned when encountering +// a line containing unexpected number of segments. +type EntryUnexpectedSegmentsError string + +func (e EntryUnexpectedSegmentsError) Error() string { + return fmt.Sprintf("unexpected segments in entry %q", string(e)) +} + +// An Entry represents one line of ldd(1) output. type Entry struct { - Name string `json:"name,omitempty"` - Path string `json:"path,omitempty"` + // File name of required object. + Name string `json:"name"` + // Absolute pathname of matched object. Only populated for the long variant. + Path *check.Absolute `json:"path,omitempty"` + // Address at which the object is loaded. Location uint64 `json:"location"` } -func Parse(p []byte) ([]*Entry, error) { - payload := strings.Split(strings.TrimSpace(string(p)), "\n") - result := make([]*Entry, len(payload)) - - for i, ent := range payload { - if len(ent) == 0 { - return nil, ErrUnexpectedNewline +// Path returns a deduplicated slice of absolute directory paths in entries. +func Path(entries []*Entry) []*check.Absolute { + p := make([]*check.Absolute, 0, len(entries)*2) + for _, entry := range entries { + if entry.Path != nil { + p = append(p, entry.Path.Dir()) } - - segment := strings.SplitN(ent, " ", 5) - - // location index - var iL int - - switch len(segment) { - case 2: // /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000) - iL = 1 - result[i] = &Entry{Name: strings.TrimSpace(segment[0])} - case 4: // libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000) - iL = 3 - if segment[1] != "=>" { - return nil, ErrUnexpectedSeparator - } - if !path.IsAbs(segment[2]) { - return nil, ErrPathNotAbsolute - } - result[i] = &Entry{ - Name: strings.TrimSpace(segment[0]), - Path: segment[2], - } - default: - return nil, EntryUnexpectedSegmentsError(ent) + if a, err := check.NewAbs(entry.Name); err == nil { + p = append(p, a.Dir()) } + } + check.SortAbs(p) + return check.CompactAbs(p) +} - if loc, err := parseLocation(segment[iL]); err != nil { - return nil, err +const ( + // entrySegmentIndexName is the index of the segment holding [Entry.Name]. + entrySegmentIndexName = 0 + // entrySegmentIndexPath is the index of the segment holding [Entry.Path], + // present only for a line describing a fully populated [Entry]. + entrySegmentIndexPath = 2 + // entrySegmentIndexSeparator is the index of the segment containing the magic bytes entrySegmentFullSeparator, + // present only for a line describing a fully populated [Entry]. + entrySegmentIndexSeparator = 1 + // entrySegmentIndexLocation is the index of the segment holding [Entry.Location] + // for a line describing a fully populated [Entry]. + entrySegmentIndexLocation = 3 + // entrySegmentIndexLocationShort is the index of the segment holding [Entry.Location] + // for a line describing only [Entry.Name]. + entrySegmentIndexLocationShort = 1 + + // entrySegmentSep is the byte separating segments in an [Entry] line. + entrySegmentSep = ' ' + // entrySegmentFullSeparator is the exact contents of the segment at index entrySegmentIndexSeparator. + entrySegmentFullSeparator = "=>" + + // entrySegmentLocationLengthMin is the minimum possible length of a segment corresponding to [Entry.Location]. + entrySegmentLocationLengthMin = 4 + // entrySegmentLocationPrefix are magic bytes prefixing a segment corresponding to [Entry.Location]. + entrySegmentLocationPrefix = "(0x" + // entrySegmentLocationSuffix is the magic byte suffixing a segment corresponding to [Entry.Location]. + entrySegmentLocationSuffix = ')' +) + +// decodeLocationSegment decodes and saves the segment corresponding to [Entry.Location]. +func (e *Entry) decodeLocationSegment(segment []byte) (err error) { + if len(segment) < entrySegmentLocationLengthMin || + segment[len(segment)-1] != entrySegmentLocationSuffix || + string(segment[:len(entrySegmentLocationPrefix)]) != entrySegmentLocationPrefix { + return ErrBadLocationFormat + } + + e.Location, err = strconv.ParseUint(string(segment[3:len(segment)-1]), 16, 64) + return +} + +// UnmarshalText parses a line of ldd(1) output and saves it to [Entry]. +func (e *Entry) UnmarshalText(data []byte) error { + var ( + segments = bytes.SplitN(data, []byte{entrySegmentSep}, 5) + // segment to pass to decodeLocationSegment + iL int + ) + + switch len(segments) { + case 2: // /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000) + iL = entrySegmentIndexLocationShort + e.Name = string(bytes.TrimSpace(segments[entrySegmentIndexName])) + + case 4: // libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000) + iL = entrySegmentIndexLocation + if string(segments[entrySegmentIndexSeparator]) != entrySegmentFullSeparator { + return ErrUnexpectedSeparator + } + if a, err := check.NewAbs(string(segments[entrySegmentIndexPath])); err != nil { + return err } else { - result[i].Location = loc + e.Path = a } + e.Name = string(bytes.TrimSpace(segments[entrySegmentIndexName])) + + default: + return EntryUnexpectedSegmentsError(data) } - return result, nil + return e.decodeLocationSegment(segments[iL]) } -func parseLocation(s string) (uint64, error) { - if len(s) < 4 || s[len(s)-1] != ')' || s[:3] != "(0x" { - return math.MaxUint64, ErrBadLocationFormat - } - return strconv.ParseUint(s[3:len(s)-1], 16, 64) +// A Decoder reads and decodes [Entry] values from an input stream. +// +// The zero value is not safe for use. +type Decoder struct { + s *bufio.Scanner + + // Whether the current line is not the first line. + notFirst bool + // Whether s has no more tokens. + depleted bool + // Holds onto the first error encountered while parsing. + err error } + +// NewDecoder returns a new decoder that reads from r. +// +// The decoder introduces its own buffering and may read +// data from r beyond the [Entry] values requested. +func NewDecoder(r io.Reader) *Decoder { return &Decoder{s: bufio.NewScanner(r)} } + +// Scan advances the [Decoder] to the next [Entry] and +// stores the result in the value pointed to by v. +func (d *Decoder) Scan(v *Entry) bool { + if d.s == nil || d.err != nil || d.depleted { + return false + } + if !d.s.Scan() { + d.depleted = true + return false + } + + data := d.s.Bytes() + if len(data) == 0 { + if d.notFirst { + if d.s.Scan() && d.err == nil { + d.err = ErrUnexpectedNewline + } + // trailing newline is allowed (glibc) + return false + } + + // leading newline is allowed (musl) + d.notFirst = true + return d.Scan(v) + } + + d.notFirst = true + d.err = v.UnmarshalText(data) + return d.err == nil +} + +// Err returns the first non-EOF error that was encountered +// by the underlying [bufio.Scanner] or [Entry]. +func (d *Decoder) Err() error { + if d.err != nil || d.s == nil { + return d.err + } + return d.s.Err() +} + +// Decode reads from the input stream until there are no more entries +// and returns the results in a slice. +func (d *Decoder) Decode() ([]*Entry, error) { + var entries []*Entry + + e := new(Entry) + for d.Scan(e) { + entries = append(entries, e) + e = new(Entry) + } + return entries, d.Err() +} + +// Parse returns a slice of addresses to [Entry] decoded from p. +func Parse(p []byte) ([]*Entry, error) { return NewDecoder(bytes.NewReader(p)).Decode() } diff --git a/ldd/ldd_test.go b/ldd/ldd_test.go index a6044f6..0288a7c 100644 --- a/ldd/ldd_test.go +++ b/ldd/ldd_test.go @@ -1,10 +1,12 @@ package ldd_test import ( + "encoding/json" "errors" "reflect" "testing" + "hakurei.app/container/check" "hakurei.app/ldd" ) @@ -20,15 +22,19 @@ func TestParseError(t *testing.T) { libzstd.so.1 => /usr/lib/libzstd.so.1 (0x7ff71bfd2000) `, ldd.ErrUnexpectedNewline}, + {"unexpected separator", ` libzstd.so.1 = /usr/lib/libzstd.so.1 (0x7ff71bfd2000) `, ldd.ErrUnexpectedSeparator}, + {"path not absolute", ` libzstd.so.1 => usr/lib/libzstd.so.1 (0x7ff71bfd2000) -`, ldd.ErrPathNotAbsolute}, +`, &check.AbsoluteError{Pathname: "usr/lib/libzstd.so.1"}}, + {"unexpected segments", ` meow libzstd.so.1 => /usr/lib/libzstd.so.1 (0x7ff71bfd2000) `, ldd.EntryUnexpectedSegmentsError("meow libzstd.so.1 => /usr/lib/libzstd.so.1 (0x7ff71bfd2000)")}, + {"bad location format", ` libzstd.so.1 => /usr/lib/libzstd.so.1 7ff71bfd2000 `, ldd.ErrBadLocationFormat}, @@ -49,6 +55,7 @@ func TestParse(t *testing.T) { testCases := []struct { file, out string want []*ldd.Entry + paths []*check.Absolute }{ {"musl /bin/kmod", ` /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000) @@ -56,30 +63,38 @@ libzstd.so.1 => /usr/lib/libzstd.so.1 (0x7ff71bfd2000) liblzma.so.5 => /usr/lib/liblzma.so.5 (0x7ff71bf9a000) libz.so.1 => /lib/libz.so.1 (0x7ff71bf80000) libcrypto.so.3 => /lib/libcrypto.so.3 (0x7ff71ba00000) -libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`, - []*ldd.Entry{ - {"/lib/ld-musl-x86_64.so.1", "", 0x7ff71c0a4000}, - {"libzstd.so.1", "/usr/lib/libzstd.so.1", 0x7ff71bfd2000}, - {"liblzma.so.5", "/usr/lib/liblzma.so.5", 0x7ff71bf9a000}, - {"libz.so.1", "/lib/libz.so.1", 0x7ff71bf80000}, - {"libcrypto.so.3", "/lib/libcrypto.so.3", 0x7ff71ba00000}, - {"libc.musl-x86_64.so.1", "/lib/ld-musl-x86_64.so.1", 0x7ff71c0a4000}, - }}, +libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`, []*ldd.Entry{ + {"/lib/ld-musl-x86_64.so.1", nil, 0x7ff71c0a4000}, + {"libzstd.so.1", check.MustAbs("/usr/lib/libzstd.so.1"), 0x7ff71bfd2000}, + {"liblzma.so.5", check.MustAbs("/usr/lib/liblzma.so.5"), 0x7ff71bf9a000}, + {"libz.so.1", check.MustAbs("/lib/libz.so.1"), 0x7ff71bf80000}, + {"libcrypto.so.3", check.MustAbs("/lib/libcrypto.so.3"), 0x7ff71ba00000}, + {"libc.musl-x86_64.so.1", check.MustAbs("/lib/ld-musl-x86_64.so.1"), 0x7ff71c0a4000}, + }, []*check.Absolute{ + check.MustAbs("/lib"), + check.MustAbs("/usr/lib"), + }}, + {"glibc /nix/store/rc3n2r3nffpib2gqpxlkjx36frw6n34z-kmod-31/bin/kmod", ` linux-vdso.so.1 (0x00007ffed65be000) libzstd.so.1 => /nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib/libzstd.so.1 (0x00007f3199cd1000) liblzma.so.5 => /nix/store/g78jna1i5qhh8gqs4mr64648f0szqgw4-xz-5.4.7/lib/liblzma.so.5 (0x00007f3199ca2000) libc.so.6 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libc.so.6 (0x00007f3199ab5000) libpthread.so.0 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libpthread.so.0 (0x00007f3199ab0000) - /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2 (0x00007f3199da5000)`, - []*ldd.Entry{ - {"linux-vdso.so.1", "", 0x00007ffed65be000}, - {"libzstd.so.1", "/nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib/libzstd.so.1", 0x00007f3199cd1000}, - {"liblzma.so.5", "/nix/store/g78jna1i5qhh8gqs4mr64648f0szqgw4-xz-5.4.7/lib/liblzma.so.5", 0x00007f3199ca2000}, - {"libc.so.6", "/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libc.so.6", 0x00007f3199ab5000}, - {"libpthread.so.0", "/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libpthread.so.0", 0x00007f3199ab0000}, - {"/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2", "/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2", 0x00007f3199da5000}, - }}, + /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2 (0x00007f3199da5000)`, []*ldd.Entry{ + {"linux-vdso.so.1", nil, 0x00007ffed65be000}, + {"libzstd.so.1", check.MustAbs("/nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib/libzstd.so.1"), 0x00007f3199cd1000}, + {"liblzma.so.5", check.MustAbs("/nix/store/g78jna1i5qhh8gqs4mr64648f0szqgw4-xz-5.4.7/lib/liblzma.so.5"), 0x00007f3199ca2000}, + {"libc.so.6", check.MustAbs("/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libc.so.6"), 0x00007f3199ab5000}, + {"libpthread.so.0", check.MustAbs("/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libpthread.so.0"), 0x00007f3199ab0000}, + {"/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2", check.MustAbs("/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2"), 0x00007f3199da5000}, + }, []*check.Absolute{ + check.MustAbs("/nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib"), + check.MustAbs("/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib"), + check.MustAbs("/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64"), + check.MustAbs("/nix/store/g78jna1i5qhh8gqs4mr64648f0szqgw4-xz-5.4.7/lib"), + }}, + {"glibc /usr/bin/xdg-dbus-proxy", ` linux-vdso.so.1 (0x00007725f5772000) libglib-2.0.so.0 => /usr/lib/libglib-2.0.so.0 (0x00007725f55d5000) @@ -93,31 +108,46 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`, libmount.so.1 => /usr/lib/libmount.so.1 (0x00007725f5076000) libffi.so.8 => /usr/lib/libffi.so.8 (0x00007725f506b000) /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007725f5774000) - libblkid.so.1 => /usr/lib/libblkid.so.1 (0x00007725f5032000)`, - []*ldd.Entry{ - {"linux-vdso.so.1", "", 0x00007725f5772000}, - {"libglib-2.0.so.0", "/usr/lib/libglib-2.0.so.0", 0x00007725f55d5000}, - {"libgio-2.0.so.0", "/usr/lib/libgio-2.0.so.0", 0x00007725f5406000}, - {"libgobject-2.0.so.0", "/usr/lib/libgobject-2.0.so.0", 0x00007725f53a6000}, - {"libgcc_s.so.1", "/usr/lib/libgcc_s.so.1", 0x00007725f5378000}, - {"libc.so.6", "/usr/lib/libc.so.6", 0x00007725f5187000}, - {"libpcre2-8.so.0", "/usr/lib/libpcre2-8.so.0", 0x00007725f50e8000}, - {"libgmodule-2.0.so.0", "/usr/lib/libgmodule-2.0.so.0", 0x00007725f50df000}, - {"libz.so.1", "/usr/lib/libz.so.1", 0x00007725f50c6000}, - {"libmount.so.1", "/usr/lib/libmount.so.1", 0x00007725f5076000}, - {"libffi.so.8", "/usr/lib/libffi.so.8", 0x00007725f506b000}, - {"/lib64/ld-linux-x86-64.so.2", "/usr/lib64/ld-linux-x86-64.so.2", 0x00007725f5774000}, - {"libblkid.so.1", "/usr/lib/libblkid.so.1", 0x00007725f5032000}, - }}, + libblkid.so.1 => /usr/lib/libblkid.so.1 (0x00007725f5032000)`, []*ldd.Entry{ + {"linux-vdso.so.1", nil, 0x00007725f5772000}, + {"libglib-2.0.so.0", check.MustAbs("/usr/lib/libglib-2.0.so.0"), 0x00007725f55d5000}, + {"libgio-2.0.so.0", check.MustAbs("/usr/lib/libgio-2.0.so.0"), 0x00007725f5406000}, + {"libgobject-2.0.so.0", check.MustAbs("/usr/lib/libgobject-2.0.so.0"), 0x00007725f53a6000}, + {"libgcc_s.so.1", check.MustAbs("/usr/lib/libgcc_s.so.1"), 0x00007725f5378000}, + {"libc.so.6", check.MustAbs("/usr/lib/libc.so.6"), 0x00007725f5187000}, + {"libpcre2-8.so.0", check.MustAbs("/usr/lib/libpcre2-8.so.0"), 0x00007725f50e8000}, + {"libgmodule-2.0.so.0", check.MustAbs("/usr/lib/libgmodule-2.0.so.0"), 0x00007725f50df000}, + {"libz.so.1", check.MustAbs("/usr/lib/libz.so.1"), 0x00007725f50c6000}, + {"libmount.so.1", check.MustAbs("/usr/lib/libmount.so.1"), 0x00007725f5076000}, + {"libffi.so.8", check.MustAbs("/usr/lib/libffi.so.8"), 0x00007725f506b000}, + {"/lib64/ld-linux-x86-64.so.2", check.MustAbs("/usr/lib64/ld-linux-x86-64.so.2"), 0x00007725f5774000}, + {"libblkid.so.1", check.MustAbs("/usr/lib/libblkid.so.1"), 0x00007725f5032000}, + }, []*check.Absolute{ + check.MustAbs("/lib64"), + check.MustAbs("/usr/lib"), + check.MustAbs("/usr/lib64"), + }}, } for _, tc := range testCases { t.Run(tc.file, func(t *testing.T) { t.Parallel() + if got, err := ldd.Parse([]byte(tc.out)); err != nil { - t.Errorf("Parse() error = %v", err) + t.Errorf("Parse: error = %v", err) } else if !reflect.DeepEqual(got, tc.want) { - t.Errorf("Parse() got = %#v, want %#v", got, tc.want) + t.Errorf("Parse: \n%s\nwant\n%s", mustMarshalJSON(got), mustMarshalJSON(tc.want)) + } else if paths := ldd.Path(got); !reflect.DeepEqual(paths, tc.paths) { + t.Errorf("Paths: %v, want %v", paths, tc.paths) } }) } } + +// mustMarshalJSON calls [json.Marshal] and returns the resulting data. +func mustMarshalJSON(v any) []byte { + if data, err := json.Marshal(v); err != nil { + panic(err) + } else { + return data + } +} diff --git a/ldd/path.go b/ldd/path.go deleted file mode 100644 index c8290c3..0000000 --- a/ldd/path.go +++ /dev/null @@ -1,20 +0,0 @@ -package ldd - -import ( - "hakurei.app/container/check" -) - -// Path returns a deterministic, deduplicated slice of absolute directory paths in entries. -func Path(entries []*Entry) []*check.Absolute { - p := make([]*check.Absolute, 0, len(entries)*2) - for _, entry := range entries { - if a, err := check.NewAbs(entry.Path); err == nil { - p = append(p, a.Dir()) - } - if a, err := check.NewAbs(entry.Name); err == nil { - p = append(p, a.Dir()) - } - } - check.SortAbs(p) - return check.CompactAbs(p) -}