Skip to content

Commit 993fc9e

Browse files
authored
Merge pull request #1 from oceanplexian/fix/multi-extent-udf
Add multi-extent file support
2 parents 45c0c7d + d9b00d3 commit 993fc9e

13 files changed

Lines changed: 543 additions & 18 deletions

.github/workflows/go.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
go: ['1.16', '1.17', '1.18', '1.19', '1.20']
15+
go: ['1.19', '1.20', '1.21', '1.22rc1']
1616
steps:
1717

1818
- name: Set up Go ${{ matrix.go }}
@@ -42,7 +42,7 @@ jobs:
4242
run: sudo go test -v --tags=integration -coverprofile=coverage_integration.txt -covermode=atomic .
4343

4444
- name: Upload coverage to Codecov
45-
if: matrix.go == '1.20'
45+
if: matrix.go == '1.21'
4646
uses: codecov/codecov-action@v2
4747
with:
4848
files: ./coverage_unit.txt,coverage_integration.txt

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
A package for reading and creating ISO9660
77

8-
Joliet and Rock Ridge extensions are **NOT** supported.
8+
Joliet extension is **NOT** supported.
9+
10+
Experimental support for reading Rock Ridge extension is currently in the works.
11+
If you are experiencing issues, please use the v0.3 release, which ignores Rock Ridge.
912

1013
## References for the format:
1114
- [ECMA-119 1st edition (December 1986)](https://www.ecma-international.org/wp-content/uploads/ECMA-119_1st_edition_december_1986.pdf) ([Web Archive link](http://web.archive.org/web/20210122025258/https://www.ecma-international.org/wp-content/uploads/ECMA-119_1st_edition_december_1986.pdf))

fixtures/generate.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ set -eu
44
set -o pipefail
55

66
touch -d '2018-07-25 22:01:02' test.iso_source
7+
chmod 0640 test.iso_source/dir1/lorem_ipsum.txt
78
mkisofs -V my-vol-id -publisher gopher -volset test-volset-id -preparer "$(id -un)" -o test.iso test.iso_source
9+
10+
ln -s /usr/share/some-random-directory/even-deeper-path/symlink-target test.iso_source/this-is-a-symlink
811
mkisofs -R -V my-vol-id -publisher gopher -volset test-volset-id -preparer "$(id -un)" -o test_rockridge.iso test.iso_source
12+
rm test.iso_source/this-is-a-symlink

fixtures/test.iso

0 Bytes
Binary file not shown.

fixtures/test_rockridge.iso

2 KB
Binary file not shown.

go.mod

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
module github.com/kdomanski/iso9660
22

3-
go 1.16
3+
go 1.19
44

5-
require github.com/stretchr/testify v1.7.0
5+
require github.com/stretchr/testify v1.8.4
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.1 // indirect
9+
github.com/pmezard/go-difflib v1.0.0 // indirect
10+
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
)

go.sum

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
44
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6-
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
7-
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
5+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
6+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
87
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
98
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
11-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
9+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

image_reader.go

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,32 @@ func (i *Image) RootDir() (*File, error) {
6262
return nil, os.ErrNotExist
6363
}
6464

65+
// RootDir returns the label of the first Primary Volume
66+
func (i *Image) Label() (string, error) {
67+
for _, vd := range i.volumeDescriptors {
68+
if vd.Type() == volumeTypePrimary {
69+
return string(vd.Primary.VolumeIdentifier), nil
70+
}
71+
}
72+
return "", os.ErrNotExist
73+
}
74+
75+
// extent describes a single contiguous region of data on the disc.
76+
type extent struct {
77+
Location int32
78+
Length uint32
79+
}
80+
6581
// File is a os.FileInfo-compatible wrapper around an ISO9660 directory entry
6682
type File struct {
6783
ra io.ReaderAt
6884
de *DirectoryEntry
6985
children []*File
7086
isRootDir bool
7187
susp *SUSPMetadata
88+
// extents holds all extents for multi-extent files (ECMA-119 9.1.6).
89+
// For single-extent files this is nil and de.ExtentLocation/ExtentLength are used directly.
90+
extents []extent
7291
}
7392

7493
var _ os.FileInfo = &File{}
@@ -79,6 +98,12 @@ func (f *File) hasRockRidge() bool {
7998

8099
// IsDir returns true if the entry is a directory or false otherwise
81100
func (f *File) IsDir() bool {
101+
if f.hasRockRidge() {
102+
if mode, err := f.de.SystemUseEntries.GetPosixAttr(); err == nil {
103+
return mode&os.ModeDir != 0
104+
}
105+
}
106+
82107
return f.de.FileFlags&dirFlagDir != 0
83108
}
84109

@@ -92,8 +117,15 @@ func (f *File) ModTime() time.Time {
92117
return time.Time(f.de.RecordingDateTime)
93118
}
94119

95-
// Mode returns os.FileMode flag set with the os.ModeDir flag enabled in case of directories
120+
// Mode returns file mode when available.
121+
// Otherwise it returns os.FileMode flag set with the os.ModeDir flag enabled in case of directories.
96122
func (f *File) Mode() os.FileMode {
123+
if f.hasRockRidge() {
124+
if mode, err := f.de.SystemUseEntries.GetPosixAttr(); err == nil {
125+
return mode
126+
}
127+
}
128+
97129
var mode os.FileMode
98130
if f.IsDir() {
99131
mode |= os.ModeDir
@@ -135,8 +167,16 @@ func (f *File) Name() string {
135167
return fileIdentifier
136168
}
137169

138-
// Size returns the size in bytes of the extent occupied by the file or directory
170+
// Size returns the size in bytes of the extent occupied by the file or directory.
171+
// For multi-extent files, this returns the total size across all extents.
139172
func (f *File) Size() int64 {
173+
if len(f.extents) > 0 {
174+
var total int64
175+
for _, ext := range f.extents {
176+
total += int64(ext.Length)
177+
}
178+
return total
179+
}
140180
return int64(f.de.ExtentLength)
141181
}
142182

@@ -158,6 +198,11 @@ func (f *File) GetAllChildren() ([]*File, error) {
158198

159199
baseOffset := uint32(f.de.ExtentLocation) * sectorSize
160200

201+
// pendingExtents collects extents for a multi-extent file (ECMA-119 9.1.6).
202+
// When we see directory records with the multi-extent flag set, we accumulate
203+
// their extents here until we reach the final record (without the flag).
204+
var pendingExtents []extent
205+
161206
buffer := make([]byte, sectorSize)
162207
for bytesProcessed := uint32(0); bytesProcessed < uint32(f.de.ExtentLength); bytesProcessed += sectorSize {
163208
if _, err := f.ra.ReadAt(buffer, int64(baseOffset+bytesProcessed)); err != nil {
@@ -212,12 +257,34 @@ func (f *File) GetAllChildren() ([]*File, error) {
212257

213258
i += entryLength
214259

215-
newFile := &File{ra: f.ra,
260+
// Check for multi-extent flag (ECMA-119 9.1.6, bit 7 of FileFlags).
261+
// When set, this directory record is not the final one for this file.
262+
// Consecutive records should have their extents concatenated.
263+
if newDE.FileFlags&dirFlagMultiExtent != 0 {
264+
pendingExtents = append(pendingExtents, extent{
265+
Location: newDE.ExtentLocation,
266+
Length: newDE.ExtentLength,
267+
})
268+
continue
269+
}
270+
271+
newFile := &File{
272+
ra: f.ra,
216273
de: newDE,
217274
children: nil,
218275
susp: f.susp.Clone(),
219276
}
220277

278+
// If we accumulated multi-extent records, finalize them now.
279+
if len(pendingExtents) > 0 {
280+
pendingExtents = append(pendingExtents, extent{
281+
Location: newDE.ExtentLocation,
282+
Length: newDE.ExtentLength,
283+
})
284+
newFile.extents = pendingExtents
285+
pendingExtents = nil
286+
}
287+
221288
f.children = append(f.children, newFile)
222289
}
223290
}
@@ -264,11 +331,22 @@ func (f *File) GetDotEntry() (*File, error) {
264331

265332
// Reader returns a reader that allows to read the file's data.
266333
// If File is a directory, it returns nil.
334+
// For multi-extent files (ECMA-119 9.1.6), the returned reader
335+
// seamlessly reads across all extents.
267336
func (f *File) Reader() io.Reader {
268337
if f.IsDir() {
269338
return nil
270339
}
271340

341+
if len(f.extents) > 1 {
342+
readers := make([]io.Reader, len(f.extents))
343+
for i, ext := range f.extents {
344+
offset := int64(ext.Location) * int64(sectorSize)
345+
readers[i] = io.NewSectionReader(f.ra, offset, int64(ext.Length))
346+
}
347+
return io.MultiReader(readers...)
348+
}
349+
272350
baseOffset := int64(f.de.ExtentLocation) * int64(sectorSize)
273351
return io.NewSectionReader(f.ra, baseOffset, int64(f.de.ExtentLength))
274352
}

image_reader_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package iso9660
55

66
import (
77
"io"
8+
"io/fs"
89
"os"
910
"testing"
1011
"time"
@@ -30,6 +31,10 @@ func TestImageReader(t *testing.T) {
3031
assert.Equal(t, volumeTypeTerminator, image.volumeDescriptors[1].Header.Type)
3132
}
3233

34+
label, err := image.Label()
35+
assert.NoError(t, err)
36+
assert.Equal(t, "my-vol-id", label)
37+
3338
rootDir, err := image.RootDir()
3439
assert.NoError(t, err)
3540
assert.True(t, rootDir.IsDir())
@@ -141,7 +146,12 @@ func TestImageReaderSUSP(t *testing.T) {
141146

142147
children, err := rootDir.GetChildren()
143148
assert.NoError(t, err)
144-
assert.Len(t, children, 4)
149+
assert.Len(t, children, 5)
150+
151+
// symlink
152+
symlink := children[4]
153+
assert.Equal(t, "this-is-a-symlink", symlink.Name())
154+
assert.Equal(t, os.ModeSymlink, symlink.Mode()&os.ModeSymlink)
145155

146156
dir1 := children[1]
147157
assert.Equal(t, "dir1", dir1.Name())
@@ -154,6 +164,7 @@ func TestImageReaderSUSP(t *testing.T) {
154164
loremFile := dir1Children[0]
155165
assert.Equal(t, "lorem_ipsum.txt", loremFile.Name())
156166
assert.Equal(t, int64(446), loremFile.Size())
167+
assert.Equal(t, fs.FileMode(0640), loremFile.Mode().Perm(), "expected mode %o, got %o", 0640, loremFile.Mode().Perm())
157168
assert.NotNil(t, loremFile.susp)
158169
assert.True(t, loremFile.susp.HasRockRidge)
159170

image_writer.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,25 @@ func (iw *ImageWriter) AddFile(data io.Reader, filePath string) error {
7979
return err
8080
}
8181

82+
func failIfSymlink(path string) error {
83+
info, err := os.Lstat(path)
84+
if err != nil {
85+
return err
86+
}
87+
88+
if info.Mode()&os.ModeSymlink != 0 {
89+
return fmt.Errorf("%q is a symlink - these are not yet supported", path)
90+
}
91+
92+
return nil
93+
}
94+
8295
// AddLocalFile adds a file identified by its path to the ImageWriter's staging area.
8396
func (iw *ImageWriter) AddLocalFile(origin, target string) error {
97+
if err := failIfSymlink(origin); err != nil {
98+
return err
99+
}
100+
84101
directoryPath, fileName := manglePath(target)
85102

86103
if err := os.MkdirAll(path.Join(iw.stagingDir, directoryPath), 0755); err != nil {

0 commit comments

Comments
 (0)