diff --git a/cmd/iso9660/main.go b/cmd/iso9660/main.go index f5d2cca..3cbf3ae 100644 --- a/cmd/iso9660/main.go +++ b/cmd/iso9660/main.go @@ -6,6 +6,8 @@ import ( "path/filepath" "runtime" "strings" + + "github.com/hooklift/iso9660" ) func main() { @@ -43,24 +45,24 @@ func main() { panic(err) } - //freader := fi.Sys().(io.Reader) - //f, err := os.Create(fp) - //if err != nil { - // panic(err) - //} - //defer func() { - // if err := f.Close(); err != nil { - // panic(err) - // } - //}() + freader := fi.Sys().(io.Reader) + f, err := os.Create(fp) + if err != nil { + panic(err) + } + defer func() { + if err := f.Close(); err != nil { + panic(err) + } + }() - //if err := f.Chmod(fi.Mode()); err != nil { - // panic(err) - //} + if err := f.Chmod(fi.Mode()); err != nil { + panic(err) + } - //if _, err := io.Copy(f, freader); err != nil { - // panic(err) - //} + if _, err := io.Copy(f, freader); err != nil { + panic(err) + } } } diff --git a/fixtures/test.iso b/fixtures/test.iso index a990e29..c8cff29 100644 Binary files a/fixtures/test.iso and b/fixtures/test.iso differ diff --git a/iso9660.go b/iso9660.go index 7376a81..a1c7d85 100644 --- a/iso9660.go +++ b/iso9660.go @@ -10,6 +10,8 @@ package iso9660 import ( + "bytes" + "encoding/binary" "io" "os" "strings" @@ -20,14 +22,14 @@ import ( // accessing ISO9660 file stats type FileStat struct { DirectoryRecord - FileID string + fileID string // We have the raw image here only to be able to access file extents image io.ReadSeeker } // Name returns the file's name. func (fi *FileStat) Name() string { - return strings.TrimSpace(fi.FileID) + return strings.TrimSpace(fi.fileID) } // Size returns the file size in bytes @@ -47,9 +49,9 @@ func (fi *FileStat) ModTime() time.Time { // IsDir tells whether the file is a directory or not. func (fi *FileStat) IsDir() bool { - // if (fi.FileFlags & isDirectory) == 0 { - // return true - // } + if (fi.FileFlags & isDirectory) == 0 { + return true + } return false } @@ -59,7 +61,32 @@ func (fi *FileStat) Sys() interface{} { return nil } - return nil + // Saves the current position within the ISO image. This is so we can + // restore it once the file content is read. By doing this we allow + // reader.Next() to keep working normally. + curOffset, err := fi.image.Seek(0, os.SEEK_CUR) + if err != nil { + panic(err) + } + + buf := make([]byte, fi.ExtentLengthBE) + _, err = fi.image.Seek(int64(fi.ExtentLocationBE*fi.ExtentLengthBE), os.SEEK_SET) + if err != nil { + panic(err) + } + + err = binary.Read(fi.image, binary.BigEndian, &buf) + if err != nil { + panic(err) + } + + // Restores original position within the ISO image after reading file's content. + _, err = fi.image.Seek(curOffset, os.SEEK_SET) + if err != nil { + panic(err) + } + + return bytes.NewReader(buf) } const ( @@ -133,38 +160,37 @@ type PrimaryVolumePart1 struct { _ [8]byte // Amount of data available on the CD-ROM. Ignores little-endian order. // Takes big-endian encoded value. - _ int32 - VolumeSpaceSize int32 - Unused2 [32]byte + VolumeSpaceSizeLE int32 + VolumeSpaceSizeBE int32 + Unused2 [32]byte // The size of the set in this logical volume (number of disks). Ignores // little-endian order. Takes big-endian encoded value. - _ int16 - VolumeSetSize int16 - + VolumeSetSizeLE int16 + VolumeSetSizeBE int16 // The number of this disk in the Volume Set. Ignores little-endian order. // Takes big-endian encoded value. - _ int16 - VolumeSeqNumber int16 + VolumeSeqNumberLE int16 + VolumeSeqNumberBE int16 // The size in bytes of a logical block. NB: This means that a logical block // on a CD could be something other than 2 KiB! - _ int16 - LogicalBlkSize int16 + LogicalBlkSizeLE int16 + LogicalBlkSizeBE int16 // The size in bytes of the path table. Ignores little-endian order. // Takes big-endian encoded value. - _ int32 - PathTableSize int32 + PathTableSizeLE int32 + PathTableSizeBE int32 // LBA location of the path table. The path table pointed to contains only // little-endian values. - _ int32 + LocPathTableLE int32 // LBA location of the optional path table. The path table pointed to contains // only little-endian values. Zero means that no optional path table exists. - _ int32 + LocOptPathTableLE int32 // LBA location of the path table. The path table pointed to contains // only big-endian values. - LocMPathTable int32 + LocPathTableBE int32 // LBA location of the optional path table. The path table pointed to contains // only big-endian values. Zero means that no optional path table exists. - LocOptMPathTable int32 + LocOptPathTableBE int32 } // DirectoryRecord describes the characteristics of a file or directory, @@ -187,11 +213,11 @@ type DirectoryRecord struct { // the file's extent. ExtendedAttrLen byte // Location of extent (Logical Block Address) in both-endian format. - _ uint32 - ExtentLocation uint32 + ExtentLocationLE uint32 + ExtentLocationBE uint32 // Data length (size of extent) in both-endian format. - _ uint32 - ExtentLength uint32 + ExtentLengthLE uint32 + ExtentLengthBE uint32 // Date and the time of the day at which the information in the Extent // described by the Directory Record was recorded. RecordedTime [7]byte @@ -206,21 +232,21 @@ type DirectoryRecord struct { InterleaveGapSize byte // Volume sequence number - the volume that this extent is recorded on, in // 16 bit both-endian format. - _ uint16 - VolumeSeqNumber uint16 + VolumeSeqNumberLE uint16 + VolumeSeqNumberBE uint16 // Length of file identifier (file name). This terminates with a ';' // character followed by the file ID number in ASCII coded decimal ('1'). FileIDLength byte - // // The interpretation of this field depends as follows on the setting of the - // // Directory bit of the File Flags field. If set to ZERO, it shall mean: - // // - // // − The field shall specify an identification for the file. - // // − The characters in this field shall be d-characters or d1-characters, SEPARATOR 1, SEPARATOR 2. - // // − The field shall be recorded as specified in 7.5. If set to ONE, it shall mean: - // // − The field shall specify an identification for the directory. - // // − The characters in this field shall be d-characters or d1-characters, or only a (00) byte, or only a (01) byte. - // // − The field shall be recorded as specified in 7.6. - //fileID string + // The interpretation of this field depends as follows on the setting of the + // Directory bit of the File Flags field. If set to ZERO, it shall mean: + // + // − The field shall specify an identification for the file. + // − The characters in this field shall be d-characters or d1-characters, SEPARATOR 1, SEPARATOR 2. + // − The field shall be recorded as specified in 7.5. If set to ONE, it shall mean: + // − The field shall specify an identification for the directory. + // − The characters in this field shall be d-characters or d1-characters, or only a (00) byte, or only a (01) byte. + // − The field shall be recorded as specified in 7.6. + // fileID string } // PrimaryVolumePart2 represents the Primary Volume Descriptor half after the diff --git a/reader.go b/reader.go index d79b434..3759281 100644 --- a/reader.go +++ b/reader.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "os" + + "github.com/c4milo/gotoolkit" ) var ( @@ -19,8 +21,16 @@ var ( // Reader defines the state of the ISO9660 image reader. It needs to be instantiated // from its constructor. type Reader struct { + // File descriptor to the opened ISO image image io.ReadSeeker - pvd PrimaryVolume + // Copy of unencoded Primary Volume Descriptor + pvd PrimaryVolume + // Queue used to walk through file system iteratively + queue gotoolkit.Queue + // Current sector + sector uint32 + // Current bytes read from current sector. + read uint32 } // NewReader creates a new ISO9660 reader. @@ -41,12 +51,15 @@ func NewReader(rs io.ReadSeeker) (*Reader, error) { } if volDesc.Type == primaryVol { + // backs up to the beginning of the sector again in order to unpack + // the entire primary volume descriptor more easily. if _, err := rs.Seek(offset, os.SEEK_SET); err != nil { return nil, ErrCorruptedImage(err) } reader := new(Reader) reader.image = rs + reader.queue = new(gotoolkit.SliceQueue) if err := reader.unpackPVD(); err != nil { return nil, ErrCorruptedImage(err) @@ -62,87 +75,153 @@ func NewReader(rs io.ReadSeeker) (*Reader, error) { } } -// Next moves onto the next directory record if there is any +// Next moves onto the next directory record using breadth-first search one step +// at the time. It first moves func (r *Reader) Next() (os.FileInfo, error) { if r == nil { - panic("missing reader instance") + panic("missing reader instance. Use the constructor to create a Reader instance.") } - drecord := new(DirectoryRecord) - if err := r.unpackDRecord(drecord); err != nil { - return nil, err + if r.queue.IsEmpty() { + return nil, io.EOF } - fi := &FileStat{image: r.image, DirectoryRecord: *drecord} - - return fi, nil -} - -// unpackPVD unpacks Primary Volume Descriptor in three phases. This is -// because the root directory record is a variable-length record and Go's binary -// package doesn't support unpacking variable-length structs easily. -func (r *Reader) unpackPVD() error { - // Unpack first half - var pvd1 PrimaryVolumePart1 - if err := binary.Read(r.image, binary.BigEndian, &pvd1); err != nil { - return ErrCorruptedImage(err) + // We only dequeue the directory when it does not contain more children + // or when it is empty and there is no children to go over. + item, err := r.queue.Peek() + if err != nil { + panic(err) + } + fi := item.(FileStat) + + fmt.Println(r.sector) + // If there is no more entries in the current directory, dequeue it + // and move on the next directory in the queue. + if r.read > fi.ExtentLengthBE { + r.read = 0 + r.queue.Dequeue() + r.sector = 0 + return &fi, nil } - r.pvd.PrimaryVolumePart1 = pvd1 - // Unpack root directory record - var drecord DirectoryRecord - if err := r.unpackDRecord(&drecord); err != nil { - return ErrCorruptedImage(err) + if r.sector == 0 { + r.sector = fi.ExtentLocationBE } - r.pvd.DirectoryRecord = drecord - // Unpack second half - var pvd2 PrimaryVolumePart2 - if err := binary.Read(r.image, binary.BigEndian, &pvd2); err != nil { - return ErrCorruptedImage(err) + var drecord FileStat + var len byte + // We need this loop to skip parent and self directories: .. and . + for { + // If we are at the end of the sector, move onto the next one. + if (r.read % sectorSize) == 0 { + r.sector++ + _, err := r.image.Seek(int64(r.sector*sectorSize), os.SEEK_SET) + if err != nil { + return nil, ErrCorruptedImage(err) + } + } + + if len, err = r.unpackDRecord(&drecord); err != nil && err != io.EOF { + return nil, ErrCorruptedImage(err) + } + r.read += uint32(len) + + if err == io.EOF { + // directory record is empty, sector lost, move onto next sector. + rsize := (sectorSize - (r.read % sectorSize)) + buf := make([]byte, rsize) + if err := binary.Read(r.image, binary.BigEndian, buf); err != nil { + return nil, ErrCorruptedImage(err) + } + r.read += rsize + } + + if drecord.fileID != "\x00" && drecord.fileID != "\x01" { + if drecord.IsDir() { + r.queue.Enqueue(drecord) + } else { + drecord.image = r.image + } + break + } } - r.pvd.PrimaryVolumePart2 = pvd2 - return nil + return &drecord, nil } -func (r *Reader) unpackDRecord(drecord *DirectoryRecord) error { +// unpackDRecord unpacks directory record bits into Go's struct +func (r *Reader) unpackDRecord(fi *FileStat) (byte, error) { + // Gets the directory record length var len byte if err := binary.Read(r.image, binary.BigEndian, &len); err != nil { - return ErrCorruptedImage(err) + return len, ErrCorruptedImage(err) } if len == 0 { - return nil + return len + 1, io.EOF } - if err := binary.Read(r.image, binary.BigEndian, drecord); err != nil { - return ErrCorruptedImage(err) + // Reads directory record into Go struct + var drecord DirectoryRecord + if err := binary.Read(r.image, binary.BigEndian, &drecord); err != nil { + return len, ErrCorruptedImage(err) } + fi.DirectoryRecord = drecord + // Gets the name name := make([]byte, drecord.FileIDLength) if err := binary.Read(r.image, binary.BigEndian, name); err != nil { - return ErrCorruptedImage(err) + return len, ErrCorruptedImage(err) } - //drecord.FileID = string(name) - //fmt.Printf("name: ->%s<-\n", name) + fi.fileID = string(name) - //Padding field as per section 9.1.12 in ECMA-119 + // Padding field as per section 9.1.12 in ECMA-119 if (drecord.FileIDLength % 2) == 0 { var zero byte if err := binary.Read(r.image, binary.BigEndian, &zero); err != nil { - return ErrCorruptedImage(err) + return len, ErrCorruptedImage(err) } } // System use field as per section 9.1.13 in ECMA-119 + // Directory record has 34 bytes in addition to the name's + // variable length and the padding field mentioned in section 9.1.12 totalLen := 34 + drecord.FileIDLength - (drecord.FileIDLength % 2) sysUseLen := int64(len - totalLen) if sysUseLen > 0 { sysData := make([]byte, sysUseLen) if err := binary.Read(r.image, binary.BigEndian, sysData); err != nil { - return ErrCorruptedImage(err) + return len, ErrCorruptedImage(err) } } + return len, nil +} + +// unpackPVD unpacks Primary Volume Descriptor in three phases. This is +// because the root directory record is a variable-length record and Go's binary +// package doesn't support unpacking variable-length structs easily. +func (r *Reader) unpackPVD() error { + // Unpack first half + var pvd1 PrimaryVolumePart1 + if err := binary.Read(r.image, binary.BigEndian, &pvd1); err != nil { + return ErrCorruptedImage(err) + } + r.pvd.PrimaryVolumePart1 = pvd1 + + // Unpack root directory record + var drecord FileStat + if _, err := r.unpackDRecord(&drecord); err != nil { + return ErrCorruptedImage(err) + } + r.pvd.DirectoryRecord = drecord.DirectoryRecord + r.queue.Enqueue(drecord) + + // Unpack second half + var pvd2 PrimaryVolumePart2 + if err := binary.Read(r.image, binary.BigEndian, &pvd2); err != nil { + return ErrCorruptedImage(err) + } + r.pvd.PrimaryVolumePart2 = pvd2 + return nil } diff --git a/reader_test.go b/reader_test.go index bcb5cfd..a06635a 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1,6 +1,8 @@ package iso9660 import ( + "fmt" + "io" "os" "strings" "testing" @@ -19,21 +21,21 @@ func TestNewReader(t *testing.T) { assert.Equals(t, 1, int(r.pvd.Version)) assert.Equals(t, "Mac OS X", strings.TrimSpace(string(r.pvd.SystemID[:]))) assert.Equals(t, "my-vol-id", strings.TrimSpace(string(r.pvd.ID[:]))) - assert.Equals(t, 179, int(r.pvd.VolumeSpaceSize)) - assert.Equals(t, 1, int(r.pvd.VolumeSetSize)) - assert.Equals(t, 1, int(r.pvd.VolumeSeqNumber)) - assert.Equals(t, 2048, int(r.pvd.LogicalBlkSize)) - assert.Equals(t, 34, int(r.pvd.PathTableSize)) - assert.Equals(t, 21, int(r.pvd.LocMPathTable)) - assert.Equals(t, 0, int(r.pvd.LocOptMPathTable)) + assert.Equals(t, 181, int(r.pvd.VolumeSpaceSizeBE)) + assert.Equals(t, 1, int(r.pvd.VolumeSetSizeBE)) + assert.Equals(t, 1, int(r.pvd.VolumeSeqNumberBE)) + assert.Equals(t, 2048, int(r.pvd.LogicalBlkSizeBE)) + assert.Equals(t, 46, int(r.pvd.PathTableSizeBE)) + assert.Equals(t, 21, int(r.pvd.LocPathTableBE)) + assert.Equals(t, 0, int(r.pvd.LocOptPathTableBE)) // Test root directory record values assert.Equals(t, 0, int(r.pvd.DirectoryRecord.ExtendedAttrLen)) - assert.Equals(t, 23, int(r.pvd.DirectoryRecord.ExtentLocation)) - assert.Equals(t, 2048, int(r.pvd.DirectoryRecord.ExtentLength)) + assert.Equals(t, 23, int(r.pvd.DirectoryRecord.ExtentLocationBE)) + assert.Equals(t, 2048, int(r.pvd.DirectoryRecord.ExtentLengthBE)) assert.Equals(t, 2, int(r.pvd.DirectoryRecord.FileFlags)) assert.Equals(t, 0, int(r.pvd.DirectoryRecord.FileUnitSize)) assert.Equals(t, 0, int(r.pvd.DirectoryRecord.InterleaveGapSize)) - assert.Equals(t, 1, int(r.pvd.DirectoryRecord.VolumeSeqNumber)) + assert.Equals(t, 1, int(r.pvd.DirectoryRecord.VolumeSeqNumberBE)) assert.Equals(t, 1, int(r.pvd.DirectoryRecord.FileIDLength)) // Test second half of primary volume descriptor assert.Equals(t, "my-vol-id", strings.TrimSpace(string(r.pvd.ID[:]))) @@ -44,14 +46,6 @@ func TestNewReader(t *testing.T) { assert.Equals(t, 1, int(r.pvd.FileStructVersion)) } -// func TestUnpackPVD(t *testing.T) { -// image, err := os.Open("./fixtures/test.iso") -// defer image.Close() -// reader, err := NewReader(image) -// assert.Ok(t, err) -// reader.pvd -// } - // func TestUnpackRootDRecord(t *testing.T) { // image, err := os.Open("./fixtures/test.iso") // defer image.Close() @@ -67,5 +61,19 @@ func TestNewReader(t *testing.T) { // } func TestUnpackChildren(t *testing.T) { + image, err := os.Open("./fixtures/test.iso") + defer image.Close() + reader, err := NewReader(image) + assert.Ok(t, err) + + for { + fi, err := reader.Next() + if err == io.EOF { + break + } + assert.Ok(t, err) + f := fi.(*FileStat) + fmt.Printf("->%#v<-\n", f.fileID) + } }