diff options
-rw-r--r-- | cmd/btrfs-rec/inspect_lsfiles.go | 294 | ||||
-rw-r--r-- | lib/btrfs/btrfsitem/item_fileextent.go | 24 | ||||
-rw-r--r-- | lib/btrfs/btrfsitem/item_inode.go | 2 | ||||
-rw-r--r-- | lib/btrfs/btrfsitem/item_inoderef.go | 36 | ||||
-rw-r--r-- | lib/btrfs/btrfsitem/items_gen.go | 4 | ||||
-rw-r--r-- | lib/btrfs/io4_fs.go | 16 | ||||
-rw-r--r-- | lib/btrfsprogs/btrfsinspect/print_tree.go | 8 | ||||
-rwxr-xr-x | scripts/main.sh | 2 |
8 files changed, 240 insertions, 146 deletions
diff --git a/cmd/btrfs-rec/inspect_lsfiles.go b/cmd/btrfs-rec/inspect_lsfiles.go index 44b4363..e706c1f 100644 --- a/cmd/btrfs-rec/inspect_lsfiles.go +++ b/cmd/btrfs-rec/inspect_lsfiles.go @@ -5,8 +5,12 @@ package main import ( + "bufio" + "errors" "fmt" - "reflect" + "io" + "os" + "path" "strings" "github.com/datawire/dlib/derror" @@ -15,7 +19,7 @@ import ( "git.lukeshu.com/btrfs-progs-ng/lib/btrfs" "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsitem" - "git.lukeshu.com/btrfs-progs-ng/lib/containers" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfsprogs/btrfsutil" "git.lukeshu.com/btrfs-progs-ng/lib/maps" ) @@ -26,12 +30,22 @@ func init() { Short: "A listing of all files in the filesystem", Args: cliutil.WrapPositionalArgs(cobra.NoArgs), }, - RunE: func(fs *btrfs.FS, _ *cobra.Command, _ []string) error { - printSubvol(fs, "", "", "/", btrfs.Key{ - ObjectID: btrfs.FS_TREE_OBJECTID, - ItemType: btrfsitem.ROOT_ITEM_KEY, - Offset: 0, + RunE: func(fs *btrfs.FS, cmd *cobra.Command, _ []string) (err error) { + defer func() { + if r := derror.PanicToError(recover()); r != nil { + fmt.Printf("\n\n%+v\n", r) + err = fmt.Errorf("panicked") + } + }() + ctx := cmd.Context() + + out := bufio.NewWriter(os.Stdout) + printSubvol(out, "", true, "/", &btrfs.Subvolume{ + FS: btrfsutil.NewBrokenTrees(ctx, fs), + TreeID: btrfs.FS_TREE_OBJECTID, }) + out.Flush() + return nil }, }) @@ -44,145 +58,181 @@ const ( tL = "└── " ) -func printSubvol(fs *btrfs.FS, prefix0, prefix1, name string, key btrfs.Key) { - root, err := fs.TreeLookup(btrfs.ROOT_TREE_OBJECTID, key) - if err != nil { - fmt.Printf("%s%q error: could not look up root %v: %v\n", prefix0, name, key, err) - return +func printText(out io.Writer, prefix string, isLast bool, name, text string) { + first, rest := tT, tl + if isLast { + first, rest = tL, tS + } + for i, line := range strings.Split(fmt.Sprintf("%q %s", name, text), "\n") { + _, _ = io.WriteString(out, prefix) + if i == 0 { + _, _ = io.WriteString(out, first) + } else { + _, _ = io.WriteString(out, rest) + } + _, _ = io.WriteString(out, line) + _, _ = io.WriteString(out, "\n") } - rootBody := root.Body.(btrfsitem.Root) - - printDir(fs, root.Key.ObjectID, prefix0, prefix1, name, rootBody.RootDirID) } -func printDir(fs *btrfs.FS, fsTree btrfs.ObjID, prefix0, prefix1, dirName string, dirInode btrfs.ObjID) { - var errs derror.MultiError - items, err := fs.TreeSearchAll(fsTree, func(key btrfs.Key) int { - return containers.NativeCmp(dirInode, key.ObjectID) - }) +func printSubvol(out io.Writer, prefix string, isLast bool, name string, subvol *btrfs.Subvolume) { + rootInode, err := subvol.GetRootInode() if err != nil { - errs = append(errs, fmt.Errorf("read dir: %w", err)) + printText(out, prefix, isLast, name, "err="+err.Error()) + return } - var dirInodeDat btrfsitem.Inode - var dirInodeDatOK bool - membersByIndex := make(map[uint64]btrfsitem.DirEntry) - membersByNameHash := make(map[uint64]btrfsitem.DirEntry) - for _, item := range items { - switch item.Key.ItemType { - case btrfsitem.INODE_ITEM_KEY: - if dirInodeDatOK { - if !reflect.DeepEqual(dirInodeDat, item.Body.(btrfsitem.Inode)) { - errs = append(errs, fmt.Errorf("read dir: multiple inodes")) - } - continue - } - dirInodeDat = item.Body.(btrfsitem.Inode) - dirInodeDatOK = true - case btrfsitem.INODE_REF_KEY: - // TODO - case btrfsitem.DIR_ITEM_KEY: - entry := item.Body.(btrfsitem.DirEntry) - namehash := btrfsitem.NameHash(entry.Name) - if namehash != item.Key.Offset { - errs = append(errs, fmt.Errorf("read dir: direntry crc32c mismatch: key=%#x crc32c(%q)=%#x", - item.Key.Offset, entry.Name, namehash)) - continue - } - if other, exists := membersByNameHash[namehash]; exists { - if !reflect.DeepEqual(entry, other) { - if string(entry.Name) == string(other.Name) { - errs = append(errs, fmt.Errorf("read dir: multiple instances of direntry crc32c(%q)=%#x", - entry.Name, namehash)) - } else { - errs = append(errs, fmt.Errorf("read dir: multiple instances of direntry crc32c(%q|%q)=%#x", - other.Name, entry.Name, namehash)) - } - } - continue - } - membersByNameHash[btrfsitem.NameHash(entry.Name)] = entry - case btrfsitem.DIR_INDEX_KEY: - index := item.Key.Offset - entry := item.Body.(btrfsitem.DirEntry) - if other, exists := membersByIndex[index]; exists { - if !reflect.DeepEqual(entry, other) { - errs = append(errs, fmt.Errorf("read dir: multiple instances of direntry index %v", index)) - } - continue - } - membersByIndex[index] = entry - //case btrfsitem.XATTR_ITEM_KEY: - default: - errs = append(errs, fmt.Errorf("TODO: handle item type %v", item.Key.ItemType)) - } + dir, err := subvol.LoadDir(rootInode) + if err != nil { + printText(out, prefix, isLast, name, "err="+err.Error()) + return } - fmt.Printf("%s%q\t[ino=%d", - prefix0, dirName, dirInode) - if dirInodeDatOK { - fmt.Printf("\tuid=%d\tgid=%d\tsize=%d]\n", - dirInodeDat.UID, dirInodeDat.GID, dirInodeDat.Size) + printDir(out, prefix, isLast, name, dir) +} + +func fmtInode(inode btrfs.BareInode) string { + var mode btrfsitem.StatMode + if inode.InodeItem == nil { + inode.Errs = append(inode.Errs, errors.New("missing INODE_ITEM")) } else { - err := fmt.Errorf("read dir: no inode data") - if len(items) == 0 && len(errs) == 1 { - err = errs[0] - errs = nil - } - fmt.Printf("]\terror: %v\n", err) + mode = inode.InodeItem.Mode } - for i, index := range maps.SortedKeys(membersByIndex) { - entry := membersByIndex[index] - namehash := btrfsitem.NameHash(entry.Name) - if other, ok := membersByNameHash[namehash]; ok { - if !reflect.DeepEqual(entry, other) { - errs = append(errs, fmt.Errorf("read dir: index=%d disagrees with crc32c(%q)=%#x", - index, entry.Name, namehash)) - } - delete(membersByNameHash, namehash) + ret := fmt.Sprintf("ino=%v mode=%v", inode.Inode, mode) + if len(inode.Errs) > 0 { + errStr := inode.Errs.Error() + if strings.Contains(errStr, "\n") { + ret += " err=\\\n" + errStr } else { - errs = append(errs, fmt.Errorf("read dir: no DIR_ITEM crc32c(%q)=%#x for DIR_INDEX index=%d", - entry.Name, namehash, index)) - } - p0, p1 := tT, tl - if (i == len(membersByIndex)-1) && (len(membersByNameHash) == 0) && (len(errs) == 0) { - p0, p1 = tL, tS + ret += " err=" + errStr } - printDirEntry(fs, fsTree, prefix1+p0, prefix1+p1, entry) } - for _, namehash := range maps.SortedKeys(membersByNameHash) { - entry := membersByNameHash[namehash] - errs = append(errs, fmt.Errorf("read dir: no DIR_INDEX for DIR_ITEM crc32c(%q)=%#x", - entry.Name, namehash)) - printDirEntry(fs, fsTree, prefix1+tT, prefix1+tl, entry) + return ret +} + +func printDir(out io.Writer, prefix string, isLast bool, name string, dir *btrfs.Dir) { + printText(out, prefix, isLast, name, fmtInode(dir.BareInode)) + if isLast { + prefix += tS + } else { + prefix += tl } - for i, err := range errs { - p0, p1 := tT, tl - if i == len(errs)-1 { - p0, p1 = tL, tS - } - fmt.Printf("%serror: %s\n", prefix1+p0, strings.ReplaceAll(err.Error(), "\n", prefix1+p1+" \n")) + for i, childName := range maps.SortedKeys(dir.ChildrenByName) { + printDirEntry( + out, + prefix, + i == len(dir.ChildrenByName)-1, + dir.SV, + path.Join(name, childName), + dir.ChildrenByName[childName]) } } -func printDirEntry(fs *btrfs.FS, fsTree btrfs.ObjID, prefix0, prefix1 string, entry btrfsitem.DirEntry) { +func printDirEntry(out io.Writer, prefix string, isLast bool, subvol *btrfs.Subvolume, name string, entry btrfsitem.DirEntry) { if len(entry.Data) != 0 { - fmt.Printf("%s%q: error: TODO: I don't know how to handle dirent.data\n", - prefix0, entry.Name) - return + panic(fmt.Errorf("TODO: I don't know how to handle dirent.data: %q", name)) } switch entry.Type { case btrfsitem.FT_DIR: switch entry.Location.ItemType { case btrfsitem.INODE_ITEM_KEY: - printDir(fs, fsTree, prefix0, prefix1, string(entry.Name), entry.Location.ObjectID) + dir, err := subvol.LoadDir(entry.Location.ObjectID) + if err != nil { + printText(out, prefix, isLast, name, "err="+err.Error()) + return + } + printDir(out, prefix, isLast, name, dir) case btrfsitem.ROOT_ITEM_KEY: - key := entry.Location - key.Offset = 0 - printSubvol(fs, prefix0, prefix1, string(entry.Name), key) + printSubvol(out, prefix, isLast, name, &btrfs.Subvolume{ + FS: subvol.FS, + TreeID: entry.Location.ObjectID, + }) default: - fmt.Printf("%s%q\t[location=%v type=%v] error: I'm not sure how to print a %v directory\n", - prefix0, entry.Name, entry.Location, entry.Type, entry.Location.ItemType) + panic(fmt.Errorf("TODO: I don't know how to handle an FT_DIR with location.ItemType=%v: %q", + entry.Location.ItemType, name)) + } + case btrfsitem.FT_SYMLINK: + if entry.Location.ItemType != btrfsitem.INODE_ITEM_KEY { + panic(fmt.Errorf("TODO: I don't know how to handle an FT_SYMLINK with location.ItemType=%v: %q", + entry.Location.ItemType, name)) + } + file, err := subvol.LoadFile(entry.Location.ObjectID) + if err != nil { + printText(out, prefix, isLast, name, "err="+err.Error()) + return + } + printSymlink(out, prefix, isLast, name, file) + case btrfsitem.FT_REG_FILE: + if entry.Location.ItemType != btrfsitem.INODE_ITEM_KEY { + panic(fmt.Errorf("TODO: I don't know how to handle an FT_REG_FILE with location.ItemType=%v: %q", + entry.Location.ItemType, name)) + } + file, err := subvol.LoadFile(entry.Location.ObjectID) + if err != nil { + printText(out, prefix, isLast, name, "err="+err.Error()) + return + } + printFile(out, prefix, isLast, name, file) + case btrfsitem.FT_SOCK: + if entry.Location.ItemType != btrfsitem.INODE_ITEM_KEY { + panic(fmt.Errorf("TODO: I don't know how to handle an FT_SOCK with location.ItemType=%v: %q", + entry.Location.ItemType, name)) + } + file, err := subvol.LoadFile(entry.Location.ObjectID) + if err != nil { + printText(out, prefix, isLast, name, "err="+err.Error()) + return + } + printSocket(out, prefix, isLast, name, file) + case btrfsitem.FT_FIFO: + if entry.Location.ItemType != btrfsitem.INODE_ITEM_KEY { + panic(fmt.Errorf("TODO: I don't know how to handle an FT_FIFO with location.ItemType=%v: %q", + entry.Location.ItemType, name)) + } + file, err := subvol.LoadFile(entry.Location.ObjectID) + if err != nil { + printText(out, prefix, isLast, name, "err="+err.Error()) + return } + printPipe(out, prefix, isLast, name, file) default: - fmt.Printf("%s%q\t[location=%v type=%v]\n", prefix0, entry.Name, entry.Location, entry.Type) + panic(fmt.Errorf("TODO: I don't know how to handle an fileType=%v: %q", + entry.Type, name)) + } +} + +func printSymlink(out io.Writer, prefix string, isLast bool, name string, file *btrfs.File) { + var tgt []byte + if file.InodeItem != nil { + var err error + tgt, err = io.ReadAll(io.NewSectionReader(file, 0, file.InodeItem.Size)) + if err != nil { + file.Errs = append(file.Errs, err) + } + } + printText(out, prefix, isLast, name, fmt.Sprintf( + "-> %q : %s", + tgt, + fmtInode(file.BareInode))) +} + +func printFile(out io.Writer, prefix string, isLast bool, name string, file *btrfs.File) { + if file.InodeItem != nil { + if _, err := io.Copy(io.Discard, io.NewSectionReader(file, 0, file.InodeItem.Size)); err != nil { + file.Errs = append(file.Errs, err) + } + } + printText(out, prefix, isLast, name, fmtInode(file.BareInode)) +} + +func printSocket(out io.Writer, prefix string, isLast bool, name string, file *btrfs.File) { + if file.InodeItem != nil && file.InodeItem.Size > 0 { + panic(fmt.Errorf("TODO: I don't know how to handle a socket with size>0: %q", name)) + } + printText(out, prefix, isLast, name, fmtInode(file.BareInode)) +} + +func printPipe(out io.Writer, prefix string, isLast bool, name string, file *btrfs.File) { + if file.InodeItem != nil && file.InodeItem.Size > 0 { + panic(fmt.Errorf("TODO: I don't know how to handle a pipe with size>0: %q", name)) } + printText(out, prefix, isLast, name, fmtInode(file.BareInode)) } diff --git a/lib/btrfs/btrfsitem/item_fileextent.go b/lib/btrfs/btrfsitem/item_fileextent.go index a69c67a..b7e394e 100644 --- a/lib/btrfs/btrfsitem/item_fileextent.go +++ b/lib/btrfs/btrfsitem/item_fileextent.go @@ -28,20 +28,22 @@ type FileExtent struct { // EXTENT_DATA=108 binstruct.End `bin:"off=0x15"` // only one of these, depending on .Type - BodyInline []byte `bin:"-"` // .Type == FILE_EXTENT_INLINE - BodyExtent struct { // .Type == FILE_EXTENT_REG or FILE_EXTENT_PREALLOC - // Position and size of extent within the device - DiskByteNr btrfsvol.LogicalAddr `bin:"off=0x0, siz=0x8"` - DiskNumBytes btrfsvol.AddrDelta `bin:"off=0x8, siz=0x8"` + BodyInline []byte `bin:"-"` // .Type == FILE_EXTENT_INLINE + BodyExtent FileExtentExtent `bin:"-"` // .Type == FILE_EXTENT_REG or FILE_EXTENT_PREALLOC +} + +type FileExtentExtent struct { + // Position and size of extent within the device + DiskByteNr btrfsvol.LogicalAddr `bin:"off=0x0, siz=0x8"` + DiskNumBytes btrfsvol.AddrDelta `bin:"off=0x8, siz=0x8"` - // Position of data within the extent - Offset btrfsvol.AddrDelta `bin:"off=0x10, siz=0x8"` + // Position of data within the extent + Offset btrfsvol.AddrDelta `bin:"off=0x10, siz=0x8"` - // Decompressed/unencrypted size - NumBytes int64 `bin:"off=0x18, siz=0x8"` + // Decompressed/unencrypted size + NumBytes int64 `bin:"off=0x18, siz=0x8"` - binstruct.End `bin:"off=0x20"` - } `bin:"-"` + binstruct.End `bin:"off=0x20"` } func (o *FileExtent) UnmarshalBinary(dat []byte) (int, error) { diff --git a/lib/btrfs/btrfsitem/item_inode.go b/lib/btrfs/btrfsitem/item_inode.go index 27204f6..49b2031 100644 --- a/lib/btrfs/btrfsitem/item_inode.go +++ b/lib/btrfs/btrfsitem/item_inode.go @@ -14,7 +14,7 @@ type Inode struct { // INODE_ITEM=1 Generation internal.Generation `bin:"off=0x00, siz=0x08"` TransID int64 `bin:"off=0x08, siz=0x08"` Size int64 `bin:"off=0x10, siz=0x08"` // stat - NumBytes int64 `bin:"off=0x18, siz=0x08"` + NumBytes int64 `bin:"off=0x18, siz=0x08"` // allocated bytes, may be larger than size (or smaller if there are holes?) BlockGroup int64 `bin:"off=0x20, siz=0x08"` NLink int32 `bin:"off=0x28, siz=0x04"` // stat UID int32 `bin:"off=0x2C, siz=0x04"` // stat diff --git a/lib/btrfs/btrfsitem/item_inoderef.go b/lib/btrfs/btrfsitem/item_inoderef.go index b1eaf1b..083f19e 100644 --- a/lib/btrfs/btrfsitem/item_inoderef.go +++ b/lib/btrfs/btrfsitem/item_inoderef.go @@ -12,8 +12,40 @@ import ( ) // key.objectid = inode number of the file -// key.offset = inode number of the parent file -type InodeRef struct { // INODE_REF=12 +// key.offset = inode number of the parent directory +// +// Might have multiple entries if the same file has multiple hardlinks +// in the same directory. +type InodeRefs []InodeRef // INODE_REF=12 + +func (o *InodeRefs) UnmarshalBinary(dat []byte) (int, error) { + *o = nil + n := 0 + for n < len(dat) { + var ref InodeRef + _n, err := binstruct.Unmarshal(dat[n:], &ref) + n += _n + if err != nil { + return n, err + } + *o = append(*o, ref) + } + return n, nil +} + +func (o InodeRefs) MarshalBinary() ([]byte, error) { + var dat []byte + for _, ref := range o { + _dat, err := binstruct.Marshal(ref) + dat = append(dat, _dat...) + if err != nil { + return dat, err + } + } + return dat, nil +} + +type InodeRef struct { Index int64 `bin:"off=0x0, siz=0x8"` NameLen uint16 `bin:"off=0x8, siz=0x2"` // [ignored-when-writing] binstruct.End `bin:"off=0xa"` diff --git a/lib/btrfs/btrfsitem/items_gen.go b/lib/btrfs/btrfsitem/items_gen.go index 8573967..d21cd3e 100644 --- a/lib/btrfs/btrfsitem/items_gen.go +++ b/lib/btrfs/btrfsitem/items_gen.go @@ -57,7 +57,7 @@ var keytype2gotype = map[Type]reflect.Type{ FREE_SPACE_EXTENT_KEY: reflect.TypeOf(Empty{}), FREE_SPACE_INFO_KEY: reflect.TypeOf(FreeSpaceInfo{}), INODE_ITEM_KEY: reflect.TypeOf(Inode{}), - INODE_REF_KEY: reflect.TypeOf(InodeRef{}), + INODE_REF_KEY: reflect.TypeOf(InodeRefs{}), METADATA_ITEM_KEY: reflect.TypeOf(Metadata{}), ORPHAN_ITEM_KEY: reflect.TypeOf(Empty{}), PERSISTENT_ITEM_KEY: reflect.TypeOf(DevStats{}), @@ -91,7 +91,7 @@ func (FreeSpaceBitmap) isItem() {} func (FreeSpaceHeader) isItem() {} func (FreeSpaceInfo) isItem() {} func (Inode) isItem() {} -func (InodeRef) isItem() {} +func (InodeRefs) isItem() {} func (Metadata) isItem() {} func (Root) isItem() {} func (RootRef) isItem() {} diff --git a/lib/btrfs/io4_fs.go b/lib/btrfs/io4_fs.go index ad489c7..0ef922b 100644 --- a/lib/btrfs/io4_fs.go +++ b/lib/btrfs/io4_fs.go @@ -191,9 +191,15 @@ func (ret *Dir) populate() { for _, item := range ret.OtherItems { switch item.Key.ItemType { case btrfsitem.INODE_REF_KEY: + body := item.Body.(btrfsitem.InodeRefs) + if len(body) != 1 { + ret.Errs = append(ret.Errs, fmt.Errorf("INODE_REF item with %d entries on a directory", + len(body))) + continue + } ref := InodeRef{ Inode: ObjID(item.Key.Offset), - InodeRef: item.Body.(btrfsitem.InodeRef), + InodeRef: body[0], } if ret.DotDot != nil { if !reflect.DeepEqual(ref, *ret.DotDot) { @@ -336,13 +342,13 @@ func (ret *File) populate() { } pos += size } - if ret.InodeItem != nil && pos != ret.InodeItem.Size { - if ret.InodeItem.Size > pos { + if ret.InodeItem != nil && pos != ret.InodeItem.NumBytes { + if ret.InodeItem.NumBytes > pos { ret.Errs = append(ret.Errs, fmt.Errorf("extent gap from %v to %v", - pos, ret.InodeItem.Size)) + pos, ret.InodeItem.NumBytes)) } else { ret.Errs = append(ret.Errs, fmt.Errorf("extent mapped past end of file from %v to %v", - ret.InodeItem.Size, pos)) + ret.InodeItem.NumBytes, pos)) } } } diff --git a/lib/btrfsprogs/btrfsinspect/print_tree.go b/lib/btrfsprogs/btrfsinspect/print_tree.go index ff53953..d7eb6f3 100644 --- a/lib/btrfsprogs/btrfsinspect/print_tree.go +++ b/lib/btrfsprogs/btrfsinspect/print_tree.go @@ -135,9 +135,11 @@ func printTree(ctx context.Context, out io.Writer, fs *btrfs.FS, treeID btrfs.Ob fmt.Fprintf(out, "\t\tctime %v\n", fmtTime(body.CTime)) fmt.Fprintf(out, "\t\tmtime %v\n", fmtTime(body.MTime)) fmt.Fprintf(out, "\t\totime %v\n", fmtTime(body.OTime)) - case btrfsitem.InodeRef: - fmt.Fprintf(out, "\t\tindex %v namelen %v name: %s\n", - body.Index, body.NameLen, body.Name) + case btrfsitem.InodeRefs: + for _, ref := range body { + fmt.Fprintf(out, "\t\tindex %v namelen %v name: %s\n", + ref.Index, ref.NameLen, ref.Name) + } //case btrfsitem.INODE_EXTREF_KEY: // // TODO case btrfsitem.DirEntry: diff --git a/scripts/main.sh b/scripts/main.sh index a9afde3..dbcf3ec 100755 --- a/scripts/main.sh +++ b/scripts/main.sh @@ -9,3 +9,5 @@ if ! test -s ../scratch/dump.rebuilt-mappings.json; then > ../scratch/dump.rebuilt-mappings.json \ 2> >(tee >&2 ../scratch/dump.rebuilt-mappings.log) fi +time ./btrfs-rec --pv=../scratch/dump-zero.img --mappings=../scratch/dump.rebuilt-mappings.json inspect ls-files \ + &> ../scratch/dump.ls-files.txt |