diff --git a/ldd/ldd.go b/ldd/ldd.go index 7b88e69..9c892d7 100644 --- a/ldd/ldd.go +++ b/ldd/ldd.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "strconv" + "strings" "hakurei.app/container/check" ) @@ -117,6 +118,37 @@ func (e *Entry) UnmarshalText(data []byte) error { return e.decodeLocationSegment(segments[iL]) } +// String returns the musl ldd(1) representation of [Entry]. +func (e *Entry) String() string { + // nameInvalid is used for a zero-length e.Name + const nameInvalid = "invalid" + + var buf strings.Builder + + // libzstd.so.1 => /usr/lib/libzstd.so.1 (0x7ff71bfd2000) + l := len(e.Name) + 1 + if e.Name == "" { + l += len(nameInvalid) + } + if e.Path != nil { + l += len(entrySegmentFullSeparator) + 1 + len(e.Path.String()) + 1 + } + l += len(entrySegmentLocationPrefix) + 1<<4 + 1 + buf.Grow(l) + + if e.Name != "" { + buf.WriteString(e.Name + " ") + } else { + buf.WriteString(nameInvalid + " ") + } + if e.Path != nil { + buf.WriteString(entrySegmentFullSeparator + " " + e.Path.String() + " ") + } + buf.WriteString(entrySegmentLocationPrefix + strconv.FormatUint(e.Location, 16) + string(entrySegmentLocationSuffix)) + + return buf.String() +} + // 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) diff --git a/ldd/ldd_test.go b/ldd/ldd_test.go index 2bacb7a..a994d33 100644 --- a/ldd/ldd_test.go +++ b/ldd/ldd_test.go @@ -158,6 +158,89 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`, []*ldd.Entr } } +func TestString(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + e ldd.Entry + want string + }{ + {"ld", ldd.Entry{ + Name: "/lib/ld-musl-x86_64.so.1", + Location: 0x7ff71c0a4000, + }, `/lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`}, + + {"libzstd", ldd.Entry{ + Name: "libzstd.so.1", + Path: check.MustAbs("/usr/lib/libzstd.so.1"), + Location: 0x7ff71bfd2000, + }, `libzstd.so.1 => /usr/lib/libzstd.so.1 (0x7ff71bfd2000)`}, + + {"liblzma", ldd.Entry{ + Name: "liblzma.so.5", + Path: check.MustAbs("/usr/lib/liblzma.so.5"), + Location: 0x7ff71bf9a000, + }, `liblzma.so.5 => /usr/lib/liblzma.so.5 (0x7ff71bf9a000)`}, + + {"libz", ldd.Entry{ + Name: "libz.so.1", + Path: check.MustAbs("/lib/libz.so.1"), + Location: 0x7ff71bf80000, + }, `libz.so.1 => /lib/libz.so.1 (0x7ff71bf80000)`}, + + {"libcrypto", ldd.Entry{ + Name: "libcrypto.so.3", + Path: check.MustAbs("/lib/libcrypto.so.3"), + Location: 0x7ff71ba00000, + }, `libcrypto.so.3 => /lib/libcrypto.so.3 (0x7ff71ba00000)`}, + + {"libc", ldd.Entry{ + Name: "libc.musl-x86_64.so.1", + Path: check.MustAbs("/lib/ld-musl-x86_64.so.1"), + Location: 0x7ff71c0a4000, + }, `libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`}, + + {"invalid", ldd.Entry{ + Location: 0x7ff71c0a4000, + }, `invalid (0x7ff71c0a4000)`}, + + {"invalid long", ldd.Entry{ + Path: check.MustAbs("/lib/ld-musl-x86_64.so.1"), + Location: 0x7ff71c0a4000, + }, `invalid => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + t.Run("decode", func(t *testing.T) { + if tc.e.Name == "" { + return + } + t.Parallel() + + var got ldd.Entry + if err := got.UnmarshalText([]byte(tc.want)); err != nil { + t.Fatalf("UnmarshalText: error = %v", err) + } + + if !reflect.DeepEqual(&got, &tc.e) { + t.Errorf("UnmarshalText: %#v, want %#v", got, tc.e) + } + }) + + t.Run("encode", func(t *testing.T) { + t.Parallel() + + if got := tc.e.String(); got != tc.want { + t.Errorf("String: %s, want %s", got, tc.want) + } + }) + }) + } +} + // mustMarshalJSON calls [json.Marshal] and returns the resulting data. func mustMarshalJSON(v any) []byte { if data, err := json.Marshal(v); err != nil {