Skip to content

Commit f1b0b87

Browse files
authored
Merge pull request #3 from oceanplexian/feat/joliet-support
Add Joliet supplementary volume descriptor support
2 parents 993fc9e + f4abbf0 commit f1b0b87

4 files changed

Lines changed: 120 additions & 5 deletions

File tree

fixtures/test_joliet.iso

370 KB
Binary file not shown.

image_reader.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package iso9660
22

33
import (
4+
"encoding/binary"
45
"fmt"
56
"io"
67
"os"
78
"strings"
89
"time"
10+
"unicode/utf16"
911
)
1012

1113
// Image is a wrapper around an image file that allows reading its ISO9660 data
@@ -51,9 +53,18 @@ func (i *Image) readVolumes() error {
5153
return nil
5254
}
5355

54-
// RootDir returns the File structure corresponding to the root directory
55-
// of the first primary volume
56+
// RootDir returns the File structure corresponding to the root directory.
57+
// It prefers a Joliet supplementary volume descriptor (which provides full
58+
// Unicode filenames) over the primary volume descriptor.
5659
func (i *Image) RootDir() (*File, error) {
60+
// Check for Joliet supplementary VD first.
61+
for _, vd := range i.volumeDescriptors {
62+
if vd.isJoliet() {
63+
return &File{de: vd.Primary.RootDirectoryEntry, ra: i.ra, children: nil, isRootDir: true, joliet: true}, nil
64+
}
65+
}
66+
67+
// Fall back to primary VD.
5768
for _, vd := range i.volumeDescriptors {
5869
if vd.Type() == volumeTypePrimary {
5970
return &File{de: vd.Primary.RootDirectoryEntry, ra: i.ra, children: nil, isRootDir: true}, nil
@@ -85,6 +96,7 @@ type File struct {
8596
children []*File
8697
isRootDir bool
8798
susp *SUSPMetadata
99+
joliet bool
88100
// extents holds all extents for multi-extent files (ECMA-119 9.1.6).
89101
// For single-extent files this is nil and de.ExtentLocation/ExtentLength are used directly.
90102
extents []extent
@@ -141,6 +153,15 @@ func (f *File) Name() string {
141153
}
142154
}
143155

156+
// Joliet names are already decoded to UTF-8; just strip any trailing ";1".
157+
if f.joliet {
158+
name := f.de.Identifier
159+
if idx := strings.LastIndex(name, ";"); idx >= 0 {
160+
name = name[:idx]
161+
}
162+
return name
163+
}
164+
144165
if f.IsDir() {
145166
return f.de.Identifier
146167
}
@@ -224,6 +245,11 @@ func (f *File) GetAllChildren() ([]*File, error) {
224245
return nil, err
225246
}
226247

248+
// Decode Joliet UTF-16BE identifiers to UTF-8.
249+
if f.joliet && len(newDE.Identifier) > 1 {
250+
newDE.Identifier = decodeJolietIdentifier([]byte(newDE.Identifier))
251+
}
252+
227253
// Is this a root directory '.' record?
228254
if f.isRootDir && newDE.Identifier == string([]byte{0}) {
229255
newDE.SystemUseEntries, _ = splitSystemUseEntries(newDE.SystemUse, f.ra)
@@ -273,6 +299,7 @@ func (f *File) GetAllChildren() ([]*File, error) {
273299
de: newDE,
274300
children: nil,
275301
susp: f.susp.Clone(),
302+
joliet: f.joliet,
276303
}
277304

278305
// If we accumulated multi-extent records, finalize them now.
@@ -350,3 +377,17 @@ func (f *File) Reader() io.Reader {
350377
baseOffset := int64(f.de.ExtentLocation) * int64(sectorSize)
351378
return io.NewSectionReader(f.ra, baseOffset, int64(f.de.ExtentLength))
352379
}
380+
381+
// decodeJolietIdentifier decodes a UTF-16BE encoded Joliet identifier to UTF-8.
382+
func decodeJolietIdentifier(raw []byte) string {
383+
if len(raw)%2 != 0 {
384+
return string(raw)
385+
}
386+
387+
u16 := make([]uint16, len(raw)/2)
388+
for i := range u16 {
389+
u16[i] = binary.BigEndian.Uint16(raw[2*i : 2*i+2])
390+
}
391+
392+
return string(utf16.Decode(u16))
393+
}

image_reader_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,64 @@ func TestImage(t *testing.T) {
120120
assert.Error(t, os.ErrNotExist, err)
121121
}
122122

123+
func TestImageReaderJoliet(t *testing.T) {
124+
f, err := os.Open("fixtures/test_joliet.iso")
125+
assert.NoError(t, err)
126+
defer f.Close() // nolint: errcheck
127+
128+
image, err := OpenImage(f)
129+
assert.NoError(t, err)
130+
131+
// Should have primary + supplementary (Joliet) + terminator VDs
132+
hasJoliet := false
133+
for _, vd := range image.volumeDescriptors {
134+
if vd.isJoliet() {
135+
hasJoliet = true
136+
break
137+
}
138+
}
139+
assert.True(t, hasJoliet, "expected Joliet supplementary VD")
140+
141+
// RootDir should prefer Joliet
142+
rootDir, err := image.RootDir()
143+
assert.NoError(t, err)
144+
assert.True(t, rootDir.IsDir())
145+
assert.True(t, rootDir.joliet, "root should be from Joliet VD")
146+
147+
children, err := rootDir.GetChildren()
148+
assert.NoError(t, err)
149+
150+
// Collect child names
151+
names := make([]string, len(children))
152+
for i, c := range children {
153+
names[i] = c.Name()
154+
}
155+
156+
// Should have full Joliet names, not 8.3 truncated
157+
assert.Contains(t, names, "Long Filename With Spaces.txt")
158+
assert.Contains(t, names, "short.txt")
159+
assert.Contains(t, names, "subdir")
160+
161+
// Verify subdirectory traversal works with Joliet
162+
for _, child := range children {
163+
if child.Name() == "subdir" {
164+
assert.True(t, child.IsDir())
165+
assert.True(t, child.joliet, "child dir should inherit joliet flag")
166+
167+
subChildren, err := child.GetChildren()
168+
assert.NoError(t, err)
169+
assert.Len(t, subChildren, 1)
170+
assert.Equal(t, "Another Long Name.dat", subChildren[0].Name())
171+
assert.True(t, subChildren[0].joliet)
172+
173+
// Verify file content is readable
174+
data, err := io.ReadAll(subChildren[0].Reader())
175+
assert.NoError(t, err)
176+
assert.Equal(t, "Nested file\n", string(data))
177+
}
178+
}
179+
}
180+
123181
func TestImageReaderSUSP(t *testing.T) {
124182
f, err := os.Open("fixtures/test_rockridge.iso")
125183
assert.NoError(t, err)

iso9660.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,10 @@ func (bvd *BootVolumeDescriptorBody) UnmarshalBinary(data []byte) error {
387387
}
388388

389389
type volumeDescriptor struct {
390-
Header volumeDescriptorHeader
391-
Boot *BootVolumeDescriptorBody
392-
Primary *PrimaryVolumeDescriptorBody
390+
Header volumeDescriptorHeader
391+
Boot *BootVolumeDescriptorBody
392+
Primary *PrimaryVolumeDescriptorBody
393+
EscapeSequences []byte
393394
}
394395

395396
var _ encoding.BinaryUnmarshaler = &volumeDescriptor{}
@@ -399,6 +400,17 @@ func (vd volumeDescriptor) Type() byte {
399400
return vd.Header.Type
400401
}
401402

403+
// isJoliet returns true if this is a Joliet supplementary volume descriptor.
404+
// Joliet is identified by escape sequences %/@, %/C, or %/E in bytes 88-120.
405+
func (vd volumeDescriptor) isJoliet() bool {
406+
if vd.Header.Type != volumeTypeSupplementary || len(vd.EscapeSequences) == 0 {
407+
return false
408+
}
409+
410+
seq := string(vd.EscapeSequences)
411+
return strings.Contains(seq, "%/@") || strings.Contains(seq, "%/C") || strings.Contains(seq, "%/E")
412+
}
413+
402414
// UnmarshalBinary decodes a volumeDescriptor from binary form
403415
func (vd *volumeDescriptor) UnmarshalBinary(data []byte) error {
404416
if uint32(len(data)) < sectorSize {
@@ -426,6 +438,10 @@ func (vd *volumeDescriptor) UnmarshalBinary(data []byte) error {
426438
return errors.New("partition volumes are not yet supported")
427439
case volumeTypePrimary, volumeTypeSupplementary:
428440
vd.Primary = &PrimaryVolumeDescriptorBody{}
441+
if vd.Header.Type == volumeTypeSupplementary {
442+
vd.EscapeSequences = make([]byte, 32)
443+
copy(vd.EscapeSequences, data[88:120])
444+
}
429445
return vd.Primary.UnmarshalBinary(data)
430446
case volumeTypeTerminator:
431447
return nil

0 commit comments

Comments
 (0)