Skip to content

Add multi-extent file support (ECMA-119 9.1.6)#55

Open
oceanplexian wants to merge 2 commits intokdomanski:masterfrom
oceanplexian:fix/multi-extent-udf
Open

Add multi-extent file support (ECMA-119 9.1.6)#55
oceanplexian wants to merge 2 commits intokdomanski:masterfrom
oceanplexian:fix/multi-extent-udf

Conversation

@oceanplexian
Copy link
Copy Markdown

@oceanplexian oceanplexian commented Feb 17, 2026

Add multi-extent file support

This implements multi-extent directory record support as described in ECMA-119 section 9.1.6, following the guidance @kdomanski gave in Unpackerr/unpackerr#264.

When bit 7 of the FileFlags field is set on a directory record, it means that record is not the final one for the file. Consecutive records with the flag set should have their extents collected and concatenated. This is how ISO9660 handles files larger than 4GB (since ExtentLength is a 32-bit field).

The implementation follows the same approach as the Linux kernel isofs (https://github.com/torvalds/linux/blob/v5.6/fs/isofs/inode.c#LL1279C4):

  • When parsing directory entries in GetAllChildren(), if the multi-extent flag (dirFlagMultiExtent, 0x80) is set, the entry extent info is accumulated in a pending list
  • When we hit a record without the flag (the final record), all accumulated extents plus the final one are attached to the resulting File
  • Reader() returns an io.MultiReader that spans all extents seamlessly
  • Size() returns the sum of all extent lengths

The dirFlagMultiExtent constant was already defined in the codebase but was not being used - this PR puts it to work.

Changes

  • image_reader.go: Added extent struct, extents field on File, updated GetAllChildren() to detect and merge multi-extent records, updated Reader() and Size() to handle multiple extents
  • multi_extent_test.go: Tests for multi-extent files (3 extents merged into 1 file), mixed multi-extent and regular files, and verification that single-extent files remain unchanged

Test plan

  • All existing tests pass (the TestReadWriteRecordingTimestamp failure is pre-existing and timezone-related)
  • New multi-extent tests pass
  • Single-extent file behavior is unchanged (verified by test)
  • Tested with a real 5GB multi-extent ISO created by mkisofs -iso-level 3 — file was correctly extracted with byte-perfect SHA-256 verification

Per ECMA-119 section 9.1.6, when bit 7 of the FileFlags field is set on
a directory record, it indicates the record is not the final one for that
file. Consecutive records with this flag should have their extents
collected and concatenated to form the complete file.

This is needed for files larger than 4GB (2^32 bytes), which must be
split across multiple extents due to the 32-bit ExtentLength field.

Changes:
- Added extent struct to represent a single contiguous region
- File now carries an optional extents slice for multi-extent files
- GetAllChildren() detects the multi-extent flag and merges consecutive
  directory records into a single File with multiple extents
- Reader() returns an io.MultiReader spanning all extents
- Size() returns the sum of all extent lengths
- Added tests for multi-extent files, mixed with regular files, and
  verified single-extent behavior is unchanged
Parse Joliet SVDs (identified by escape sequences %/@, %/C, %/E) and
prefer them over the primary VD in RootDir(). Directory entry identifiers
are decoded from UTF-16BE to UTF-8, giving callers full-length filenames
instead of 8.3 DOS-truncated names.

- Store escape sequences from supplementary VDs (bytes 88-120)
- Add isJoliet() helper to detect Joliet SVDs
- RootDir() prefers Joliet VD when available, falls back to primary
- GetAllChildren() decodes UTF-16BE identifiers for Joliet entries
- Name() returns decoded Joliet names directly (no 8.3 processing)
- Propagate joliet flag to child File structs
- Add test with real Joliet ISO fixture
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Mar 2, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [Unpackerr/unpackerr](https://github.com/Unpackerr/unpackerr) | minor | `0.14.5` → `0.15.0` |

---

### Release Notes

<details>
<summary>Unpackerr/unpackerr (Unpackerr/unpackerr)</summary>

### [`v0.15.0`](https://github.com/Unpackerr/unpackerr/releases/tag/v0.15.0): Version 15 in 8 years

[Compare Source](Unpackerr/unpackerr@v0.14.5...v0.15.0)

#### New Features

- Folder setting `delete_after` has a default setting of 10 minutes.
- Log file mode can now be set.
- Will now unpack multi-file 7zip archives.
- Log file is now automatically created.
- FreeBSD rc.d improvements to allow overriding username.
- Progress updates are now logged. Every 15 seconds (configurable), the app logs the current extraction percentage.
- Adds `exclude_path` to the folder watcher so you can ignore specific paths.
- Folder setting `disable_recursion` works correctly now.
- Empty download folders are removed when archives are removed.
- Golift docker image now uses alpine instead of scratch.

##### These features are thanks to [@&#8203;oceanplexian](https://github.com/oceanplexian)

- Supports non-UTF8 file encoding (Japanese, Chinese, etc).
- Improved ISO9660 ([Joilet](kdomanski/iso9660#55)) support.
- [UDF](https://github.com/golift/udf) support (like ISO, but bigger).
- Splits FLAC/CUE files for Lidarr. **This is still experimental and feedback is requested.**
- Archives with an incorrect extension may still be extracted. We now detect the file type using the first few bytes.

#### Bug Fixes

It's a pretty big list, but at the end of the day all of the bugs pretty much lead to the same problem: extraction failed. A lot of these problems have been fixed and extractions are considerably more reliable.

#### Merged Contributions

- update arch pkgbuild by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;479](Unpackerr/unpackerr#479)
- update install.sh script by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;480](Unpackerr/unpackerr#480)
- add overrides for examples and docker by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;481](Unpackerr/unpackerr#481)
- Remove expanding of URL previews by [@&#8203;BoKKeR](https://github.com/BoKKeR) in [#&#8203;500](Unpackerr/unpackerr#500)
- docs: add clarify delete\_after needing to be greater than 0 by [@&#8203;bakerboy448](https://github.com/bakerboy448) in [#&#8203;485](Unpackerr/unpackerr#485)
- fix docs by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;501](Unpackerr/unpackerr#501)
- Update linter, fix some bugs by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;503](Unpackerr/unpackerr#503)
- set default for folder delete after to 10m by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;530](Unpackerr/unpackerr#530)
- Allows changing the log file mode. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;531](Unpackerr/unpackerr#531)
- add multi-file 7z support to starr apps by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;533](Unpackerr/unpackerr#533)
- make log file on linux auto. allow dir instead of file for log file by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;536](Unpackerr/unpackerr#536)
- Update FreeBSD rc file. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;538](Unpackerr/unpackerr#538)
- Provide progress updates. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;535](Unpackerr/unpackerr#535)
- feat(folder): add exclude\_paths for watched folders by [@&#8203;nicholaskurjo](https://github.com/nicholaskurjo) in [#&#8203;572](Unpackerr/unpackerr#572)
- fix(folder): honor disable\_recursion for watched archive files by [@&#8203;nicholaskurjo](https://github.com/nicholaskurjo) in [#&#8203;573](Unpackerr/unpackerr#573)
- Silence logs when nothing configured by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;574](Unpackerr/unpackerr#574)
- fix lint by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;577](Unpackerr/unpackerr#577)
- Logs fallback to stdout by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;579](Unpackerr/unpackerr#579)
- Purge empty parent folder by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;578](Unpackerr/unpackerr#578)
- Dockerfile changes + build fix by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;580](Unpackerr/unpackerr#580)
- Polish ISO9660 support: Starr app extraction, UDF, multi-extent by [@&#8203;oceanplexian](https://github.com/oceanplexian) in [#&#8203;581](Unpackerr/unpackerr#581)
- Add split\_flac toggle for Lidarr CUE+FLAC splitting by [@&#8203;oceanplexian](https://github.com/oceanplexian) in [#&#8203;583](Unpackerr/unpackerr#583)
- fixes by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;584](Unpackerr/unpackerr#584)
- Manually import FLAC tracks into Lidarr after they're split. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;586](Unpackerr/unpackerr#586)

#### Auto Updates

- Update module github.com/prometheus/client\_golang to v1.20.5 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;483](Unpackerr/unpackerr#483)
- Update module golang.org/x/mod to v0.20.0 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;482](Unpackerr/unpackerr#482)
- Update module golang.org/x/mod to v0.22.0 - autoclosed by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;490](Unpackerr/unpackerr#490)
- Update module github.com/fsnotify/fsnotify to v1.8.0 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;498](Unpackerr/unpackerr#498)
- chore(deps): update dependency go to v1.23.4 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;506](Unpackerr/unpackerr#506)
- Update GitHub Artifact Actions (major) by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;551](Unpackerr/unpackerr#551)
- fix(deps): update golift.io/rotatorr digest to [`f6ac6fc`](Unpackerr/unpackerr@f6ac6fc) by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;575](Unpackerr/unpackerr#575)
- fix(deps): update module golift.io/cnfg to v0.2.4 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;576](Unpackerr/unpackerr#576)
- Update module golift.io/cnfg to v0.2.5 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;585](Unpackerr/unpackerr#585)
- Update GitHub Artifact Actions (major) by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;588](Unpackerr/unpackerr#588)

#### New Contributors

- [@&#8203;BoKKeR](https://github.com/BoKKeR) made their first contribution in [#&#8203;500](Unpackerr/unpackerr#500)
- [@&#8203;nicholaskurjo](https://github.com/nicholaskurjo) made their first contribution in [#&#8203;572](Unpackerr/unpackerr#572)
- [@&#8203;oceanplexian](https://github.com/oceanplexian) made their first contribution in [#&#8203;581](Unpackerr/unpackerr#581)

**Full Changelog**: <Unpackerr/unpackerr@v0.14.5...v0.15.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNS43IiwidXBkYXRlZEluVmVyIjoiNDMuMjUuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/4348
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Mar 2, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [golift/unpackerr](https://github.com/Unpackerr/unpackerr) | minor | `0.14.5` → `0.15.0` |

---

### Release Notes

<details>
<summary>Unpackerr/unpackerr (golift/unpackerr)</summary>

### [`v0.15.0`](https://github.com/Unpackerr/unpackerr/releases/tag/v0.15.0): Version 15 in 8 years

[Compare Source](Unpackerr/unpackerr@v0.14.5...v0.15.0)

##### New Features

- Folder setting `delete_after` has a default setting of 10 minutes.
- Log file mode can now be set.
- Will now unpack multi-file 7zip archives.
- Log file is now automatically created.
- FreeBSD rc.d improvements to allow overriding username.
- Progress updates are now logged. Every 15 seconds (configurable), the app logs the current extraction percentage.
- Adds `exclude_path` to the folder watcher so you can ignore specific paths.
- Folder setting `disable_recursion` works correctly now.
- Empty download folders are removed when archives are removed.
- Golift docker image now uses alpine instead of scratch.

##### These features are thanks to [@&#8203;oceanplexian](https://github.com/oceanplexian)

- Supports non-UTF8 file encoding (Japanese, Chinese, etc).
- Improved ISO9660 ([Joilet](kdomanski/iso9660#55)) support.
- [UDF](https://github.com/golift/udf) support (like ISO, but bigger).
- Splits FLAC/CUE files for Lidarr. **This is still experimental and feedback is requested.**
- Archives with an incorrect extension may still be extracted. We now detect the file type using the first few bytes.

##### Bug Fixes

It's a pretty big list, but at the end of the day all of the bugs pretty much lead to the same problem: extraction failed. A lot of these problems have been fixed and extractions are considerably more reliable.

##### Merged Contributions

- update arch pkgbuild by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;479](Unpackerr/unpackerr#479)
- update install.sh script by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;480](Unpackerr/unpackerr#480)
- add overrides for examples and docker by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;481](Unpackerr/unpackerr#481)
- Remove expanding of URL previews by [@&#8203;BoKKeR](https://github.com/BoKKeR) in [#&#8203;500](Unpackerr/unpackerr#500)
- docs: add clarify delete\_after needing to be greater than 0 by [@&#8203;bakerboy448](https://github.com/bakerboy448) in [#&#8203;485](Unpackerr/unpackerr#485)
- fix docs by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;501](Unpackerr/unpackerr#501)
- Update linter, fix some bugs by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;503](Unpackerr/unpackerr#503)
- set default for folder delete after to 10m by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;530](Unpackerr/unpackerr#530)
- Allows changing the log file mode. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;531](Unpackerr/unpackerr#531)
- add multi-file 7z support to starr apps by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;533](Unpackerr/unpackerr#533)
- make log file on linux auto. allow dir instead of file for log file by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;536](Unpackerr/unpackerr#536)
- Update FreeBSD rc file. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;538](Unpackerr/unpackerr#538)
- Provide progress updates. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;535](Unpackerr/unpackerr#535)
- feat(folder): add exclude\_paths for watched folders by [@&#8203;nicholaskurjo](https://github.com/nicholaskurjo) in [#&#8203;572](Unpackerr/unpackerr#572)
- fix(folder): honor disable\_recursion for watched archive files by [@&#8203;nicholaskurjo](https://github.com/nicholaskurjo) in [#&#8203;573](Unpackerr/unpackerr#573)
- Silence logs when nothing configured by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;574](Unpackerr/unpackerr#574)
- fix lint by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;577](Unpackerr/unpackerr#577)
- Logs fallback to stdout by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;579](Unpackerr/unpackerr#579)
- Purge empty parent folder by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;578](Unpackerr/unpackerr#578)
- Dockerfile changes + build fix by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;580](Unpackerr/unpackerr#580)
- Polish ISO9660 support: Starr app extraction, UDF, multi-extent by [@&#8203;oceanplexian](https://github.com/oceanplexian) in [#&#8203;581](Unpackerr/unpackerr#581)
- Add split\_flac toggle for Lidarr CUE+FLAC splitting by [@&#8203;oceanplexian](https://github.com/oceanplexian) in [#&#8203;583](Unpackerr/unpackerr#583)
- fixes by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;584](Unpackerr/unpackerr#584)
- Manually import FLAC tracks into Lidarr after they're split. by [@&#8203;davidnewhall](https://github.com/davidnewhall) in [#&#8203;586](Unpackerr/unpackerr#586)

##### Auto Updates

- Update module github.com/prometheus/client\_golang to v1.20.5 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;483](Unpackerr/unpackerr#483)
- Update module golang.org/x/mod to v0.20.0 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;482](Unpackerr/unpackerr#482)
- Update module golang.org/x/mod to v0.22.0 - autoclosed by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;490](Unpackerr/unpackerr#490)
- Update module github.com/fsnotify/fsnotify to v1.8.0 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;498](Unpackerr/unpackerr#498)
- chore(deps): update dependency go to v1.23.4 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;506](Unpackerr/unpackerr#506)
- Update GitHub Artifact Actions (major) by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;551](Unpackerr/unpackerr#551)
- fix(deps): update golift.io/rotatorr digest to [`f6ac6fc`](Unpackerr/unpackerr@f6ac6fc) by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;575](Unpackerr/unpackerr#575)
- fix(deps): update module golift.io/cnfg to v0.2.4 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;576](Unpackerr/unpackerr#576)
- Update module golift.io/cnfg to v0.2.5 by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;585](Unpackerr/unpackerr#585)
- Update GitHub Artifact Actions (major) by [@&#8203;renovate](https://github.com/renovate)\[bot] in [#&#8203;588](Unpackerr/unpackerr#588)

##### New Contributors

- [@&#8203;BoKKeR](https://github.com/BoKKeR) made their first contribution in [#&#8203;500](Unpackerr/unpackerr#500)
- [@&#8203;nicholaskurjo](https://github.com/nicholaskurjo) made their first contribution in [#&#8203;572](Unpackerr/unpackerr#572)
- [@&#8203;oceanplexian](https://github.com/oceanplexian) made their first contribution in [#&#8203;581](Unpackerr/unpackerr#581)

**Full Changelog**: <Unpackerr/unpackerr@v0.14.5...v0.15.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNS43IiwidXBkYXRlZEluVmVyIjoiNDMuMjUuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/4349
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant