├── cmd ├── where-is-the-superblock │ └── main.go ├── README.md ├── root-inode │ └── main.go ├── file-listing │ └── main.go ├── root-inode-extents │ └── main.go └── root-inode-entries │ └── main.go ├── efs ├── cylindergroup.go ├── extent.go ├── directory.go ├── block.go ├── inode.go └── filesystem.go ├── README.md ├── LICENSE ├── main.go └── sgi └── vh.go /cmd/where-is-the-superblock/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | ) 8 | 9 | func main() { 10 | file, _ := os.Open("./input.iso") 11 | b := make([]byte, 1024) 12 | file.Read(b) 13 | spew.Dump(b[512:1024]) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | These commands are shorter example programs from my blog post about this tool. 2 | 3 | If you want to run them, download an EFS filesystem image and put it in the project root directory as "input.iso", then execute the commands like: 4 | 5 | ``` 6 | $ go run ./cmd/root-inode/main.go 7 | ``` -------------------------------------------------------------------------------- /efs/cylindergroup.go: -------------------------------------------------------------------------------- 1 | package efs 2 | 3 | // CylinderGroup is an EFS concept but doesn't actually 4 | // end up being used in this implementation. I suspect 5 | // caching entire CylinderGroups together might be a 6 | // useful way to do it if you were actually doing a 7 | // filesystem implementation instead of a hacky "just 8 | // give me a tarball" tool :P 9 | type CylinderGroup struct { 10 | blocks []BasicBlock 11 | fs *Filesystem 12 | } 13 | -------------------------------------------------------------------------------- /cmd/root-inode/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | "github.com/sophaskins/efs2tar/efs" 8 | "github.com/sophaskins/efs2tar/sgi" 9 | ) 10 | 11 | func main() { 12 | file, _ := os.Open("./input.iso") 13 | b := make([]byte, 51200) 14 | _, _ = file.Read(b) 15 | 16 | vh := sgi.NewVolumeHeader(b) 17 | p := vh.Partitions[7] 18 | fs := efs.NewFilesystem(file, p.Blocks, p.First) 19 | spew.Dump(fs.RootInode()) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/file-listing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/sophaskins/efs2tar/efs" 8 | "github.com/sophaskins/efs2tar/sgi" 9 | ) 10 | 11 | func main() { 12 | file, _ := os.Open("./input.iso") 13 | b := make([]byte, 51200) 14 | _, _ = file.Read(b) 15 | vh := sgi.NewVolumeHeader(b) 16 | p := vh.Partitions[7] 17 | fs := efs.NewFilesystem(file, p.Blocks, p.First) 18 | fs.WalkFilesystem(func(in efs.Inode, path string) { 19 | fmt.Println(path) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/root-inode-extents/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | "github.com/sophaskins/efs2tar/efs" 8 | "github.com/sophaskins/efs2tar/sgi" 9 | ) 10 | 11 | func main() { 12 | file, _ := os.Open("./input.iso") 13 | b := make([]byte, 51200) 14 | _, _ = file.Read(b) 15 | 16 | vh := sgi.NewVolumeHeader(b) 17 | p := vh.Partitions[7] 18 | fs := efs.NewFilesystem(file, p.Blocks, p.First) 19 | spew.Dump(fs.RootInode().PayloadExtents()) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/root-inode-entries/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | "github.com/sophaskins/efs2tar/efs" 8 | "github.com/sophaskins/efs2tar/sgi" 9 | ) 10 | 11 | func main() { 12 | file, _ := os.Open("./input.iso") 13 | b := make([]byte, 51200) 14 | _, _ = file.Read(b) 15 | 16 | vh := sgi.NewVolumeHeader(b) 17 | p := vh.Partitions[7] 18 | fs := efs.NewFilesystem(file, p.Blocks, p.First) 19 | extents := fs.RootInode().PayloadExtents() 20 | blocks := fs.ExtentToBlocks(extents[0]) 21 | spew.Dump(blocks[0].ToDirectory().Entries()) 22 | } 23 | -------------------------------------------------------------------------------- /efs/extent.go: -------------------------------------------------------------------------------- 1 | package efs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // Extents are basically "pointers" to ranges of blocks that 9 | // contain the body of their parent entity (a file, directory, etc) 10 | type Extent struct { 11 | Magic uint8 12 | StartBlock uint32 13 | Length uint8 14 | NumIndirectExtents uint32 15 | } 16 | 17 | func NewExtent(raw []byte) Extent { 18 | // You can't just binary.Read this struct directly because 19 | // StartBlock and NumIndirectExtents fields are only given 20 | // 24 bits in the on-disk struct and I'm not aware of a way 21 | // to hint that information to binary.Read w/o just adding 22 | // more padding like this 23 | paddedExtent := make([]byte, 10) 24 | copy(paddedExtent[0:1], raw[0:1]) 25 | copy(paddedExtent[2:6], raw[1:5]) 26 | copy(paddedExtent[7:10], raw[5:8]) 27 | 28 | r := bytes.NewReader(paddedExtent) 29 | e := Extent{} 30 | binary.Read(r, binary.BigEndian, &e) 31 | return e 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # efs2tar 2 | 3 | `efs2tar` is a tool that converts SGI EFS-formatted filesystem images (ie, the result of `dd`-ing a whole device in to a file) in to tarballs. It was based entirely on NetBSD's `sys/fs/efs` ([source](http://cvsweb.netbsd.org/bsdweb.cgi/src/sys/fs/efs/?only_with_tag=MAIN)). 4 | 5 | ## Example usage 6 | 7 | ``` 8 | $ go install github.com/sophaskins/efs2tar 9 | $ efs2tar -in ~/my-sgi-disc.iso -out ~/my-sgi-disc.tar 10 | ``` 11 | 12 | The Internet Archive has [several discs](https://archive.org/search.php?query=sgi&and%5B%5D=mediatype%3A%22software%22&page=2) in its collections that are formatted with EFS. 13 | 14 | 15 | ## "Edge cases" not covered 16 | * any type of file other than directories and normal files (which is to say, links in particular do not work) 17 | * partition layouts other than what you'd expect to see on an SGI-produced CDROM 18 | * any sort of error handling...at all 19 | * that includes verifying magic numbers 20 | * preserving the original file permissions 21 | * I've only tested this on like, one CD -------------------------------------------------------------------------------- /efs/directory.go: -------------------------------------------------------------------------------- 1 | package efs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // Directory is the format of a block that stores 9 | // a directory listing. NetBSD's sys/fs/efs/efs_dir.h has 10 | // some detail as to how the Data field is organized 11 | type Directory struct { 12 | Magic uint16 13 | FirstUsed uint8 14 | Slots uint8 15 | Data [508]byte 16 | } 17 | 18 | type DirectoryEntry struct { 19 | InodeIndex uint32 20 | Name string 21 | } 22 | 23 | const DirectoryHeaderOffset = 4 24 | 25 | func (d Directory) Entries() []DirectoryEntry { 26 | entries := make([]DirectoryEntry, d.Slots) 27 | 28 | for i := range entries { 29 | // The "slots" at the low indexes of Data tell us where the 30 | // "entries" in the high indexes are 31 | if (d.Data[i] == 0) { 32 | continue 33 | } 34 | offset := (int(d.Data[i]) << 1) - DirectoryHeaderOffset 35 | 36 | r := bytes.NewReader(d.Data[offset:]) 37 | var inodeIndex uint32 38 | var nameLength uint8 39 | 40 | binary.Read(r, binary.BigEndian, &inodeIndex) 41 | binary.Read(r, binary.BigEndian, &nameLength) 42 | name := make([]byte, nameLength) 43 | binary.Read(r, binary.BigEndian, &name) 44 | 45 | entries[i] = DirectoryEntry{ 46 | InodeIndex: inodeIndex, 47 | Name: string(name), 48 | } 49 | } 50 | 51 | return entries 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018, Sophie Haskins 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /efs/block.go: -------------------------------------------------------------------------------- 1 | package efs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // BasicBlock is how the disk is divided up in EFS 9 | // Each BasicBlock can be: 10 | // * a collection of inodes 11 | // * part of the body of a file 12 | // * a directory listing 13 | // * a collection of indirect extents 14 | // * other things? 15 | type BasicBlock [512]byte 16 | 17 | func NewBasicBlock(raw []byte) BasicBlock { 18 | bb := BasicBlock{} 19 | copy(bb[:], raw) 20 | return bb 21 | } 22 | 23 | const maxInodesPerBlock = 4 24 | 25 | func (bb BasicBlock) ToInodes() []Inode { 26 | r := bytes.NewReader(bb[:]) 27 | inodes := make([]Inode, maxInodesPerBlock) 28 | for i := range inodes { 29 | binary.Read(r, binary.BigEndian, &inodes[i]) 30 | } 31 | return inodes 32 | } 33 | 34 | func (bb BasicBlock) ToDirectory() Directory { 35 | d := Directory{} 36 | r := bytes.NewReader(bb[:]) 37 | binary.Read(r, binary.BigEndian, &d) 38 | return d 39 | } 40 | 41 | const extentsPerBlock = 64 42 | 43 | func (bb BasicBlock) ToExtents() []Extent { 44 | // Note that not all of these extents are going to be valid - 45 | // the inode knows how many extents it should actually use 46 | // (and then discards the rest, which may contain garbage, who knows) 47 | extents := make([]Extent, extentsPerBlock) 48 | for i := range extents { 49 | extents[i] = NewExtent(bb[8*i : 8*(i+1)]) 50 | } 51 | return extents 52 | } 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "flag" 6 | "log" 7 | "os" 8 | 9 | "github.com/sophaskins/efs2tar/efs" 10 | "github.com/sophaskins/efs2tar/sgi" 11 | ) 12 | 13 | func main() { 14 | inputPath := flag.String("in", "", "the file to be read as an efs filesystem") 15 | outputPath := flag.String("out", "", "the file to written to as a tar file") 16 | flag.Parse() 17 | file, err := os.Open(*inputPath) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | b := make([]byte, 51200) 23 | _, err = file.Read(b) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | outputFile, err := os.OpenFile(*outputPath, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0755) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | tw := tar.NewWriter(outputFile) 33 | 34 | vh := sgi.NewVolumeHeader(b) 35 | p := vh.Partitions[7] 36 | fs := efs.NewFilesystem(file, p.Blocks, p.First) 37 | 38 | fs.WalkFilesystem(buildTarCallback(tw, fs)) 39 | tw.Close() 40 | } 41 | 42 | func buildTarCallback(tw *tar.Writer, fs *efs.Filesystem) func(efs.Inode, string) { 43 | return func(in efs.Inode, path string) { 44 | if path == "" { 45 | return 46 | } 47 | 48 | if in.Type() == efs.FileTypeDirectory { 49 | hdr := &tar.Header{ 50 | Name: path, 51 | Mode: 0755, 52 | Typeflag: tar.TypeDir, 53 | } 54 | if err := tw.WriteHeader(hdr); err != nil { 55 | log.Fatal(err) 56 | } 57 | } else if in.Type() == efs.FileTypeRegular { 58 | contents := fs.FileContents(in) 59 | hdr := &tar.Header{ 60 | Name: path, 61 | Mode: 0755, 62 | Size: int64(len(contents)), 63 | } 64 | if err := tw.WriteHeader(hdr); err != nil { 65 | log.Fatal(err) 66 | } 67 | if _, err := tw.Write([]byte(contents)); err != nil { 68 | log.Fatal(err) 69 | } 70 | } else if in.Type() == efs.FileTypeSymlink { 71 | contents := fs.FileContents(in) 72 | hdr := &tar.Header{ 73 | Name: path, 74 | Linkname: string(contents[:int64(len(contents))]), 75 | Typeflag: tar.TypeSymlink, 76 | } 77 | if err := tw.WriteHeader(hdr); err != nil { 78 | log.Fatal(err) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /efs/inode.go: -------------------------------------------------------------------------------- 1 | package efs 2 | 3 | type Inode struct { 4 | Mode uint16 5 | NumLinks int16 6 | UID uint16 7 | GID uint16 8 | Size int32 9 | ATime uint32 10 | MTime uint32 11 | CTime uint32 12 | Generation int32 13 | NumExtents int16 14 | Version uint8 15 | Spare uint8 16 | // Payload is a union struct - sometimes it contains extents, but 17 | // it also can contain other stuff (like link targets and device 18 | // descriptors, which are not implemented here) 19 | Payload [96]byte 20 | } 21 | 22 | const DirectExtentsLimit = 12 23 | 24 | const ( 25 | FileTypeFIFO = 010 26 | FileTypeCharacterDevice = 020 27 | FileTypeDirectory = 040 28 | FileTypeBlockDevice = 060 29 | FileTypeRegular = 0100 30 | FileTypeSymlink = 0120 31 | FileTypeSocket = 0140 32 | ) 33 | 34 | // FormatMode is the beginnings of something that could format 35 | // an Inode like `/bin/ls` does 36 | func (in Inode) FormatMode() string { 37 | modeString := "" 38 | switch in.Type() { 39 | case FileTypeFIFO: 40 | modeString += "p" 41 | case FileTypeCharacterDevice: 42 | modeString += "c" 43 | case FileTypeDirectory: 44 | modeString += "d" 45 | case FileTypeBlockDevice: 46 | modeString += "b" 47 | case FileTypeRegular: 48 | modeString += "-" 49 | case FileTypeSymlink: 50 | modeString += "l" 51 | case FileTypeSocket: 52 | modeString += "s" 53 | } 54 | 55 | return modeString 56 | } 57 | 58 | func (in Inode) Type() uint16 { 59 | return in.Mode >> 9 60 | } 61 | 62 | func (in Inode) usesDirectExtents() bool { 63 | return in.NumExtents <= DirectExtentsLimit 64 | } 65 | 66 | // payloadExtents unpacks the extents stored in the 67 | // Payload field. The number of these varies based on 68 | // whether or not we're using Direct Extents or not 69 | func (in Inode) PayloadExtents() []Extent { 70 | var extents []Extent 71 | 72 | if in.usesDirectExtents() { 73 | // the number of payload extents is just "all the 74 | // extents" if this inode uses Direct Extents 75 | extents = make([]Extent, in.NumExtents) 76 | for i := range extents { 77 | extents[i] = NewExtent(in.Payload[8*i : 8*(i+1)]) 78 | } 79 | } else { 80 | // the number of payload extents is contained in the 81 | // NumIndirectExtents field of the first payload extent 82 | // if we're using indirect extents 83 | firstExtent := NewExtent(in.Payload[0:8]) 84 | extents = make([]Extent, firstExtent.NumIndirectExtents) 85 | extents[0] = firstExtent 86 | for i := 1; i < int(extents[0].NumIndirectExtents); i++ { 87 | extents[i] = NewExtent(in.Payload[8*i : 8*(i+1)]) 88 | } 89 | } 90 | 91 | return extents 92 | } 93 | -------------------------------------------------------------------------------- /sgi/vh.go: -------------------------------------------------------------------------------- 1 | package sgi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | type VolumeHeader struct { 9 | MagicNumber uint32 10 | Root int16 11 | Swap int16 12 | Bootfile [16]byte 13 | BootDeviceParams DeviceParameters 14 | VolumeDirectory [15]FileHeader 15 | Partitions [16]Partition 16 | Checksum int32 17 | Padding int32 18 | } 19 | 20 | type DeviceParameters struct { 21 | Skew uint8 22 | Gap1 uint8 23 | Gap2 uint8 24 | SparesCylinder uint8 25 | Cylinder uint16 26 | Shd0 uint16 27 | Tracks uint16 28 | CtqDepth uint8 29 | CylindersShi uint8 30 | Unused uint16 31 | Sectors uint16 32 | SectorBytes uint16 33 | Interleave uint16 34 | Flags uint32 35 | DataRate uint32 36 | NumRetries uint32 37 | Mspw uint32 38 | Xgap1 uint16 39 | Xsync uint16 40 | XRdly uint16 41 | Xgap2 uint16 42 | Xrgate uint16 43 | Xwcont uint16 44 | } 45 | 46 | // FileHeader points to a special file stored in the volume header 47 | // These are typically not accessed from user-space - examples 48 | // of uses include the `fx` partitioning tool included on OS 49 | // installation discs 50 | type FileHeader struct { 51 | Name [8]byte 52 | Block int32 53 | Bytes int32 54 | } 55 | 56 | // Partition points to where on disk various partitions are 57 | // located. Partitions _can_ overlap, and they don't _have_ 58 | // to be filesystems (eg, swap) 59 | type Partition struct { 60 | Blocks int32 61 | First int32 62 | Type PartitionType 63 | } 64 | 65 | type PartitionType int32 66 | 67 | const ( 68 | VolumeHeaderPartition = PartitionType(0) 69 | TrackRepl = PartitionType(1) 70 | Repl = PartitionType(2) 71 | Raw = PartitionType(3) 72 | BSD = PartitionType(4) 73 | SystemV = PartitionType(5) 74 | Volume = PartitionType(6) 75 | EFS = PartitionType(7) 76 | LVol = PartitionType(8) 77 | RLVol = PartitionType(9) 78 | XFS = PartitionType(10) 79 | XFSLog = PartitionType(11) 80 | XLV = PartitionType(12) 81 | XVM = PartitionType(13) 82 | ) 83 | 84 | func (pt PartitionType) String() string { 85 | switch pt { 86 | case VolumeHeaderPartition: 87 | return "VolumeHeader" 88 | case TrackRepl: 89 | return "TrackRepl" 90 | case Repl: 91 | return "Repl" 92 | case Raw: 93 | return "Raw" 94 | case BSD: 95 | return "BSD" 96 | case SystemV: 97 | return "SystemV" 98 | case Volume: 99 | return "Volume" 100 | case EFS: 101 | return "EFS" 102 | case LVol: 103 | return "LogicalVolume" 104 | case RLVol: 105 | return "RLVolume" 106 | case XFS: 107 | return "XFS" 108 | case XFSLog: 109 | return "XFSLog" 110 | case XLV: 111 | return "XLV" 112 | case XVM: 113 | return "XVM" 114 | } 115 | 116 | return "Unknown" 117 | } 118 | 119 | func NewVolumeHeader(raw []byte) VolumeHeader { 120 | r := bytes.NewReader(raw) 121 | vh := VolumeHeader{} 122 | binary.Read(r, binary.BigEndian, &vh) 123 | return vh 124 | } 125 | -------------------------------------------------------------------------------- /efs/filesystem.go: -------------------------------------------------------------------------------- 1 | package efs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "os" 7 | ) 8 | 9 | const BlockSize = 512 10 | 11 | type Filesystem struct { 12 | device *os.File 13 | size int32 // size in blocks 14 | offset int32 // offset in blocks 15 | sb *SuperBlock 16 | } 17 | 18 | type SuperBlock struct { 19 | Size int32 // filesystem size (in BasicBlocks) 20 | FirstCG int32 // BasicBlock offset of the first CylinderGroup 21 | CGSize int32 // CylinderGroup size (in BasicBlocks) 22 | CGInodeSize int16 // Number of BasicBlocks per CylinderGroup that are Inodes 23 | Sectors int16 // sectors per track 24 | Heads int16 // heads per cylinder 25 | CGCount int16 // CylinderGroups in the filesystem 26 | Dirty int16 // whether an fsck is required 27 | _ int16 // padding 28 | CTime int32 // last SuperBlock updated time 29 | Magic int32 // filesystem magic number 30 | FSName [6]byte // name of the filesystem 31 | FSPack [6]byte // fs "pack" name 32 | BMSize int32 // size in bytes of bitmap 33 | FreeBlocks int32 // count of free blocks 34 | FreeInodes int32 // count of free inodes 35 | BMBlock int32 // offset of the bitmap 36 | ReplicatedSB int32 // offset of the replicated superblock 37 | LastInode int32 // last unallocated inode 38 | _ [20]int8 // padding 39 | Checksum int32 40 | } 41 | 42 | func NewFilesystem(device *os.File, size int32, offset int32) *Filesystem { 43 | fs := &Filesystem{ 44 | device: device, 45 | size: size, 46 | offset: offset, 47 | } 48 | fs.initSuperBlock() 49 | return fs 50 | } 51 | 52 | func (fs *Filesystem) WalkFilesystem(callback func(Inode, string)) { 53 | fs.walkTree(fs.RootInode(), "", callback) 54 | } 55 | 56 | func (fs *Filesystem) ExtentToBlocks(e Extent) []BasicBlock { 57 | blocks := make([]BasicBlock, e.Length) 58 | for i := range blocks { 59 | blocks[i] = fs.blockAt(int32(e.StartBlock) + int32(i)) 60 | } 61 | return blocks 62 | } 63 | 64 | func (fs *Filesystem) FileContents(in Inode) []byte { 65 | extents := fs.extents(in) 66 | fileBytes := make([]byte, in.Size) 67 | blockIndex := 0 68 | for _, extent := range extents { 69 | for _, block := range fs.ExtentToBlocks(extent) { 70 | copy(fileBytes[BlockSize*blockIndex:], block[:]) 71 | blockIndex++ 72 | } 73 | } 74 | 75 | return fileBytes 76 | } 77 | 78 | func (fs *Filesystem) extents(in Inode) []Extent { 79 | payloadExtents := in.PayloadExtents() 80 | if in.usesDirectExtents() { 81 | // if all of the extents fit inside of Payload (aka "direct extents") 82 | // we have a much simpler time reading the extents 83 | return payloadExtents 84 | } 85 | 86 | // if we have more extents than will fit in Payload, then the extents 87 | // in Payload (aka "indirect extents") point to ranges of blocks that 88 | // themselves contain the actual extents. 89 | extents := make([]Extent, in.NumExtents) 90 | extentsFetched := 0 91 | for _, indirectExtent := range payloadExtents { 92 | for _, extentBB := range fs.ExtentToBlocks(indirectExtent) { 93 | // copy respecting the length of extents saves us from 94 | // accidentally including the garbage extents at the end 95 | // of the last block (beyond NumExtents) 96 | copy(extents[extentsFetched:], extentBB.ToExtents()) 97 | extentsFetched += extentsPerBlock 98 | } 99 | } 100 | 101 | return extents 102 | } 103 | 104 | func (fs *Filesystem) walkTree(in Inode, prefix string, callback func(Inode, string)) { 105 | switch in.Type() { 106 | case FileTypeDirectory: 107 | dirExtents := fs.extents(in) 108 | // I don't _believe_ it's possible for a directory to take up more than one block 109 | // (and so also to only have one extent) but...if I'm wrong, this is where it would 110 | // make a huge problem 111 | blocks := fs.ExtentToBlocks(dirExtents[0]) 112 | for _, b := range blocks { 113 | for _, entry := range b.ToDirectory().Entries() { 114 | if entry.Name != "." && entry.Name != ".." { 115 | fs.walkTree(fs.inodeForIndex(int32(entry.InodeIndex)), prefix+"/"+entry.Name, callback) 116 | } 117 | } 118 | } 119 | fallthrough 120 | default: 121 | callback(in, prefix) 122 | } 123 | } 124 | 125 | func (fs *Filesystem) initSuperBlock() { 126 | bb := BasicBlock{} 127 | fs.device.ReadAt(bb[:], int64((fs.offset+1)*BlockSize)) 128 | r := bytes.NewReader(bb[:]) 129 | sb := SuperBlock{} 130 | binary.Read(r, binary.BigEndian, &sb) 131 | fs.sb = &sb 132 | } 133 | 134 | // InodeForIndex returns the Inode struct for the given inodeIndex 135 | // (which is to say, indexed in to the list of all inodes, _not_ a 136 | // block offset) 137 | func (fs *Filesystem) inodeForIndex(inodeIndex int32) Inode { 138 | inodeBlocksPerCG := int32(fs.sb.CGInodeSize) 139 | inodeCGIndex := inodeIndex / (inodeBlocksPerCG * maxInodesPerBlock) 140 | inodeBBinCG := inodeIndex % (inodeBlocksPerCG * maxInodesPerBlock) / maxInodesPerBlock 141 | bbIndex := fs.sb.FirstCG + inodeCGIndex*fs.sb.CGSize + inodeBBinCG 142 | bb := fs.blockAt(bbIndex) 143 | 144 | offsetInBB := inodeIndex & (maxInodesPerBlock - 1) 145 | return bb.ToInodes()[offsetInBB] 146 | } 147 | 148 | func (fs *Filesystem) RootInode() Inode { 149 | return fs.inodeForIndex(2) 150 | } 151 | 152 | func (fs *Filesystem) blockAt(index int32) BasicBlock { 153 | rawOffset := int64((fs.offset + index) * BlockSize) 154 | bb := BasicBlock{} 155 | fs.device.ReadAt(bb[:], rawOffset) 156 | 157 | return bb 158 | } 159 | --------------------------------------------------------------------------------