├── cdrom ├── .gitignore ├── Cargo.toml ├── Cargo.lock └── src │ └── lib.rs ├── testdata ├── onetrack │ ├── ccd │ │ ├── basic_image.cue │ │ ├── basic_image.img │ │ ├── basic_image.sub │ │ └── basic_image.ccd │ └── bincue │ │ ├── basic_image.cue │ │ └── basic_image.bin └── dataplusaudio │ ├── ccd │ ├── disc.bin │ ├── disc.sub │ ├── disc.cue │ └── disc.ccd │ └── bincue │ ├── disc.bin │ └── disc.cue ├── oranda.json ├── .gitignore ├── Cargo.toml ├── dist-workspace.toml ├── .github └── workflows │ ├── ci.yml │ ├── web.yml │ └── release.yml ├── README.md ├── CHANGELOG.md ├── src └── main.rs ├── Cargo.lock └── LICENSE /cdrom/.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo 2 | /target 3 | -------------------------------------------------------------------------------- /testdata/onetrack/ccd/basic_image.cue: -------------------------------------------------------------------------------- 1 | FILE "basic_image.img" BINARY 2 | TRACK 01 MODE1/2352 3 | INDEX 01 00:00:00 4 | -------------------------------------------------------------------------------- /testdata/dataplusaudio/ccd/disc.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistydemeo/cue2ccd/main/testdata/dataplusaudio/ccd/disc.bin -------------------------------------------------------------------------------- /testdata/dataplusaudio/ccd/disc.sub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistydemeo/cue2ccd/main/testdata/dataplusaudio/ccd/disc.sub -------------------------------------------------------------------------------- /testdata/onetrack/bincue/basic_image.cue: -------------------------------------------------------------------------------- 1 | FILE "basic_image.bin" BINARY 2 | TRACK 01 MODE1/2352 3 | INDEX 01 00:00:00 4 | -------------------------------------------------------------------------------- /testdata/onetrack/ccd/basic_image.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistydemeo/cue2ccd/main/testdata/onetrack/ccd/basic_image.img -------------------------------------------------------------------------------- /testdata/onetrack/ccd/basic_image.sub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistydemeo/cue2ccd/main/testdata/onetrack/ccd/basic_image.sub -------------------------------------------------------------------------------- /testdata/dataplusaudio/bincue/disc.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistydemeo/cue2ccd/main/testdata/dataplusaudio/bincue/disc.bin -------------------------------------------------------------------------------- /testdata/onetrack/bincue/basic_image.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistydemeo/cue2ccd/main/testdata/onetrack/bincue/basic_image.bin -------------------------------------------------------------------------------- /cdrom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cdrom" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | cdrom_crc = "0.1.0" 8 | cue = "3.0.1" 9 | -------------------------------------------------------------------------------- /oranda.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "path_prefix": "cue2ccd" 4 | }, 5 | "styles": { 6 | "theme": "axolight" 7 | }, 8 | "components": { 9 | "artifacts": { 10 | "cargo_dist": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testdata/dataplusaudio/bincue/disc.cue: -------------------------------------------------------------------------------- 1 | FILE "disc.bin" BINARY 2 | TRACK 01 MODE1/2352 3 | INDEX 01 00:00:00 4 | TRACK 02 AUDIO 5 | INDEX 00 00:04:16 6 | INDEX 01 00:06:16 7 | TRACK 03 AUDIO 8 | INDEX 00 00:07:16 9 | INDEX 01 00:09:16 10 | -------------------------------------------------------------------------------- /testdata/dataplusaudio/ccd/disc.cue: -------------------------------------------------------------------------------- 1 | FILE "disc.bin" BINARY 2 | TRACK 01 MODE1/2352 3 | INDEX 01 00:00:00 4 | TRACK 02 AUDIO 5 | INDEX 00 00:04:16 6 | INDEX 01 00:06:16 7 | TRACK 03 AUDIO 8 | INDEX 00 00:07:16 9 | INDEX 01 00:09:16 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo 2 | /target 3 | /testdata/onetrack/bincue/*.ccd 4 | /testdata/onetrack/bincue/*.img 5 | /testdata/onetrack/bincue/*.sub 6 | /testdata/dataplusaudio/bincue/*.ccd 7 | /testdata/dataplusaudio/bincue/*.img 8 | /testdata/dataplusaudio/bincue/*.sub 9 | # uncommitted local test data 10 | /*.7z 11 | /real_image 12 | /realtest 13 | 14 | # oranda 15 | /public 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cue2ccd" 3 | version = "1.1.0" 4 | edition = "2021" 5 | homepage = "https://www.mistys-internet.website/cue2ccd/" 6 | description = "Tool to convert BIN/CUE disc images to CCD/IMG/SUB" 7 | repository = "https://github.com/mistydemeo/cue2ccd" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | clap = { version = "4.3.4", features = ["derive"] } 13 | miette = { version = "5.6.0", features = ["fancy"] } 14 | thiserror = "1.0.40" 15 | 16 | [dependencies.cdrom] 17 | path = "cdrom" 18 | version = "0.3.0" 19 | 20 | # The profile that 'cargo dist' will build with 21 | [profile.dist] 22 | inherits = "release" 23 | lto = "thin" 24 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.30.2" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["shell", "powershell", "homebrew"] 12 | # A GitHub repo to push Homebrew formulas to 13 | tap = "mistydemeo/homebrew-formulae" 14 | # Target platforms to build apps for (Rust target-triple syntax) 15 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-gnu"] 16 | # Publish jobs to run in CI 17 | publish-jobs = ["homebrew"] 18 | # Path that installers should place binaries in 19 | install-path = "CARGO_HOME" 20 | # Whether to install an updater program 21 | install-updater = false 22 | 23 | [dist.dependencies.chocolatey] 24 | winflexbison = '*' 25 | 26 | [dist.github-custom-runners] 27 | global = "ubuntu-22.04" 28 | x86_64-unknown-linux-gnu = "ubuntu-22.04" 29 | -------------------------------------------------------------------------------- /testdata/onetrack/ccd/basic_image.ccd: -------------------------------------------------------------------------------- 1 | [CloneCD] 2 | Version=3 3 | 4 | [Disc] 5 | TocEntries=4 6 | Sessions=1 7 | DataTracksScrambled=0 8 | CDTextLength=0 9 | 10 | [Session 1] 11 | PreGapMode=1 12 | PreGapSubC=0 13 | 14 | [Entry 0] 15 | Session=1 16 | Point=0xa0 17 | ADR=0x01 18 | Control=0x04 19 | TrackNo=0 20 | AMin=0 21 | ASec=0 22 | AFrame=0 23 | ALBA=-150 24 | Zero=0 25 | PMin=1 26 | PSec=0 27 | PFrame=0 28 | PLBA=4350 29 | 30 | [Entry 1] 31 | Session=1 32 | Point=0xa1 33 | ADR=0x01 34 | Control=0x04 35 | TrackNo=0 36 | AMin=0 37 | ASec=0 38 | AFrame=0 39 | ALBA=-150 40 | Zero=0 41 | PMin=1 42 | PSec=0 43 | PFrame=0 44 | PLBA=4350 45 | 46 | [Entry 2] 47 | Session=1 48 | Point=0xa2 49 | ADR=0x01 50 | Control=0x04 51 | TrackNo=0 52 | AMin=0 53 | ASec=0 54 | AFrame=0 55 | ALBA=-150 56 | Zero=0 57 | PMin=0 58 | PSec=6 59 | PFrame=16 60 | PLBA=316 61 | 62 | [Entry 3] 63 | Session=1 64 | Point=0x01 65 | ADR=0x01 66 | Control=0x04 67 | TrackNo=0 68 | AMin=0 69 | ASec=0 70 | AFrame=0 71 | ALBA=-150 72 | Zero=0 73 | PMin=0 74 | PSec=2 75 | PFrame=0 76 | PLBA=0 77 | 78 | [TRACK 1] 79 | MODE=1 80 | INDEX 1=0 81 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # The "Normal" CI for tests and linters and whatnot 2 | name: Rust CI 3 | 4 | # Ci should be run on... 5 | on: 6 | # Every pull request (will need approval for new contributors) 7 | pull_request: 8 | # Every push to... 9 | push: 10 | branches: 11 | # The main branch 12 | - main 13 | 14 | # We want all these checks to fail if they spit out warnings 15 | env: 16 | RUSTFLAGS: -Dwarnings 17 | 18 | jobs: 19 | # Check that rustfmt is a no-op 20 | fmt: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: dtolnay/rust-toolchain@master 25 | with: 26 | toolchain: stable 27 | components: rustfmt 28 | - run: cargo fmt --all -- --check 29 | 30 | # Check that clippy is appeased 31 | clippy: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: dtolnay/rust-toolchain@master 36 | with: 37 | toolchain: stable 38 | components: clippy 39 | - uses: swatinem/rust-cache@v2 40 | - uses: actions-rs/clippy-check@v1 41 | env: 42 | PWD: ${{ env.GITHUB_WORKSPACE }} 43 | with: 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | args: --workspace --tests --examples 46 | 47 | tests: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v3 51 | - uses: dtolnay/rust-toolchain@master 52 | with: 53 | toolchain: stable 54 | - uses: swatinem/rust-cache@v2 55 | - name: Tests (cdrom) 56 | run: | 57 | cd cdrom 58 | # libcue isn't thread-safe 59 | cargo test -- --test-threads=1 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | cue2ccd 4 | ======= 5 | 6 |
7 | 8 | cue2ccd is a tool to convert BIN/CUE CD-ROM disc images into CloneCD CCD/IMG/SUB disc images. It's useful for software and devices that only support CloneCD format like Rhea/Phoebe optical drive emulators since BIN/CUE disc images are more common on the internet. 9 | 10 | Usage 11 | ----- 12 | 13 | Using cue2ccd is straightforward: just run `cue2ccd path_to_your_disc.cue`. It will produce the `.img`, `.ccd` and `.sub` files you need in the same directory 14 | as your original image, ready for use. If you prefer the generated files to be placed in a separate directory, you can specify the output path with the `--output-path` option. 15 | 16 | Limitations 17 | ----------- 18 | 19 | * cue2ccd only supports raw disc images; it doesn't support ISO files or cuesheets containing ISOs or WAV files. 20 | 21 | Building 22 | -------- 23 | 24 | To build from source, just run `cargo build` or `cargo run`. Windows users will first need to install flex and bison; this can be done using chocolatey by running [`choco install winflexbison`](https://community.chocolatey.org/packages/winflexbison). 25 | 26 | Support 27 | ------- 28 | 29 | Support is available on the [GitHub issue tracker](https://github.com/mistydemeo/cue2ccd/issues). I'm also happy to talk about feature requests. 30 | 31 | ## License 32 | 33 | cue2ccd is licensed under the GPL 2.0, which is the same license used by libcue. 34 | 35 |
36 | 37 | Contributing 38 | ------------ 39 | 40 | Help is always appreciated! You can use issues to discuss anything you'd like to work on, and pull requests are always welcome to fix bugs or add new features. 41 | 42 |
43 | -------------------------------------------------------------------------------- /testdata/dataplusaudio/ccd/disc.ccd: -------------------------------------------------------------------------------- 1 | [CloneCD] 2 | Version=3 3 | 4 | [Disc] 5 | TocEntries=6 6 | Sessions=1 7 | DataTracksScrambled=0 8 | CDTextLength=0 9 | 10 | [Session 1] 11 | PreGapMode=1 12 | PreGapSubC=0 13 | 14 | [Entry 0] 15 | Session=1 16 | Point=0xa0 17 | ADR=0x01 18 | Control=0x04 19 | TrackNo=0 20 | AMin=0 21 | ASec=0 22 | AFrame=0 23 | ALBA=-150 24 | Zero=0 25 | PMin=1 26 | PSec=0 27 | PFrame=0 28 | PLBA=4350 29 | 30 | [Entry 1] 31 | Session=1 32 | Point=0xa1 33 | ADR=0x01 34 | Control=0x00 35 | TrackNo=0 36 | AMin=0 37 | ASec=0 38 | AFrame=0 39 | ALBA=-150 40 | Zero=0 41 | PMin=3 42 | PSec=0 43 | PFrame=0 44 | PLBA=13350 45 | 46 | [Entry 2] 47 | Session=1 48 | Point=0xa2 49 | ADR=0x01 50 | Control=0x00 51 | TrackNo=0 52 | AMin=0 53 | ASec=0 54 | AFrame=0 55 | ALBA=-150 56 | Zero=0 57 | PMin=0 58 | PSec=12 59 | PFrame=16 60 | PLBA=766 61 | 62 | [Entry 3] 63 | Session=1 64 | Point=0x01 65 | ADR=0x01 66 | Control=0x04 67 | TrackNo=0 68 | AMin=0 69 | ASec=0 70 | AFrame=0 71 | ALBA=-150 72 | Zero=0 73 | PMin=0 74 | PSec=2 75 | PFrame=0 76 | PLBA=0 77 | 78 | [Entry 4] 79 | Session=1 80 | Point=0x02 81 | ADR=0x01 82 | Control=0x00 83 | TrackNo=0 84 | AMin=0 85 | ASec=0 86 | AFrame=0 87 | ALBA=-150 88 | Zero=0 89 | PMin=0 90 | PSec=8 91 | PFrame=16 92 | PLBA=466 93 | 94 | [Entry 5] 95 | Session=1 96 | Point=0x03 97 | ADR=0x01 98 | Control=0x00 99 | TrackNo=0 100 | AMin=0 101 | ASec=0 102 | AFrame=0 103 | ALBA=-150 104 | Zero=0 105 | PMin=0 106 | PSec=11 107 | PFrame=16 108 | PLBA=691 109 | 110 | [TRACK 1] 111 | MODE=1 112 | INDEX 1=0 113 | [TRACK 2] 114 | MODE=0 115 | INDEX 0=316 116 | INDEX 1=466 117 | [TRACK 3] 118 | MODE=0 119 | INDEX 0=541 120 | INDEX 1=691 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 1.1.0 (2025-11-08) 2 | 3 | This release contains a brand-new feature: subchannel-based protection emulation! If you specfiy a specific protection scheme when running cue2ccd, it will generate the subchannel data in the right nonstandard way the protection scheme expects. This can be useful for people who have created BIN/CUE copies of protected CDs and have lost the original subchannel data they need for it to work. 4 | 5 | In this first release, DiscGuard v1 and DiscGuard v2 are supported; future versions may add support for libcrypt and SecuROM. 6 | 7 | This feature was contributed by @HeroponRikiBestest. 8 | 9 | # Version 1.0.3 (2025-05-26) 10 | 11 | This release fixes two minor bugs with subchannel generation. These caused certain bits of timing information to be off by a single sector. It's unlikely this caused any discs not to work, but it may have caused very minor audio sync issues for certain discs. 12 | 13 | * Fixes an off-by-one error in the index location in the subchannel. (@HeroponRikiBestest - #30) 14 | * Fixes an error in the position where the P subchannel should end. (@mistydemeo - #32) 15 | 16 | # Version 1.0.2 (2025-05-20) 17 | 18 | This fixes several bugs: 19 | 20 | * Bad `.img` files were being generated for single-file images containing multiple tracks. (#21) 21 | * Removes a "syntax error" message when parsing most cue sheets. There wasn't an issue with the cue sheets themselves; this was a bug in the cue sheet parser cue2ccd uses. 22 | 23 | # Version 1.0.1 (2024-12-19) 24 | 25 | This fixes a few bugs from the initial release: 26 | 27 | * Subcode data was being generated incorrectly for split images. 28 | * Output filenames were generated incorrectly for cue sheets containing periods in their names. 29 | 30 | # Version 1.0.0 (2024-12-14) 31 | 32 | This is the initial release. 33 | -------------------------------------------------------------------------------- /cdrom/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "cc" 7 | version = "1.2.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" 10 | dependencies = [ 11 | "shlex", 12 | ] 13 | 14 | [[package]] 15 | name = "cdrom" 16 | version = "0.3.0" 17 | dependencies = [ 18 | "cdrom_crc", 19 | "cue", 20 | ] 21 | 22 | [[package]] 23 | name = "cdrom_crc" 24 | version = "0.1.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "b79de1ea79db6578ee25a7bcc8fddc58f958e6063aed54f426907d9d9f97895e" 27 | 28 | [[package]] 29 | name = "cmake" 30 | version = "0.1.51" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" 33 | dependencies = [ 34 | "cc", 35 | ] 36 | 37 | [[package]] 38 | name = "cue" 39 | version = "3.0.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "690eedca1eb72586e5359d32c43e8e7497aeed1c469b8b07b4eb8500056d3287" 42 | dependencies = [ 43 | "cue-sys", 44 | "libc", 45 | ] 46 | 47 | [[package]] 48 | name = "cue-sys" 49 | version = "2.0.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "aacab32605a00e807f2fe1f8c5ecfab91da08d34269bcd7319ca60c41a4a806f" 52 | dependencies = [ 53 | "cmake", 54 | "libc", 55 | ] 56 | 57 | [[package]] 58 | name = "libc" 59 | version = "0.2.146" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" 62 | 63 | [[package]] 64 | name = "shlex" 65 | version = "1.3.0" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 68 | -------------------------------------------------------------------------------- /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | # Workflow to build your docs with oranda (and mdbook) 2 | # and deploy them to Github Pages 3 | name: Web 4 | 5 | # We're going to push to the gh-pages branch, so we need that permission 6 | permissions: 7 | contents: write 8 | 9 | # What situations do we want to build docs in? 10 | # All of these work independently and can be removed / commented out 11 | # if you don't want oranda/mdbook running in that situation 12 | on: 13 | # Check that a PR didn't break docs! 14 | # 15 | # Note that the "Deploy to Github Pages" step won't run in this mode, 16 | # so this won't have any side-effects. But it will tell you if a PR 17 | # completely broke oranda/mdbook. Sadly we don't provide previews (yet)! 18 | pull_request: 19 | 20 | # Whenever something gets pushed to main, update the docs! 21 | # This is great for getting docs changes live without cutting a full release. 22 | # 23 | # Note that if you're using cargo-dist, this will "race" the Release workflow 24 | # that actually builds the Github Release that oranda tries to read (and 25 | # this will almost certainly complete first). As a result you will publish 26 | # docs for the latest commit but the oranda landing page won't know about 27 | # the latest release. The workflow_run trigger below will properly wait for 28 | # cargo-dist, and so this half-published state will only last for ~10 minutes. 29 | # 30 | # If you only want docs to update with releases, disable this, or change it to 31 | # a "release" branch. You can, of course, also manually trigger a workflow run 32 | # when you want the docs to update. 33 | push: 34 | branches: 35 | - main 36 | 37 | # Whenever a workflow called "Release" completes, update the docs! 38 | # 39 | # If you're using cargo-dist, this is recommended, as it will ensure that 40 | # oranda always sees the latest release right when it's available. Note 41 | # however that Github's UI is wonky when you use workflow_run, and won't 42 | # show this workflow as part of any commit. You have to go to the "actions" 43 | # tab for your repo to see this one running (the gh-pages deploy will also 44 | # only show up there). 45 | workflow_run: 46 | workflows: [ "Release" ] 47 | types: 48 | - completed 49 | 50 | # Alright, let's do it! 51 | jobs: 52 | web: 53 | name: Build and deploy site and docs 54 | runs-on: ubuntu-latest 55 | steps: 56 | # Setup 57 | - uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 0 60 | - uses: dtolnay/rust-toolchain@stable 61 | - uses: swatinem/rust-cache@v2 62 | 63 | # If you use any mdbook plugins, here's the place to install them! 64 | 65 | # Install and run oranda (and mdbook)! 66 | # 67 | # This will write all output to ./public/ (including copying mdbook's output to there). 68 | - name: Install and run oranda 69 | run: | 70 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.6.1/oranda-installer.sh | sh 71 | oranda build 72 | 73 | # Deploy to our gh-pages branch (creating it if it doesn't exist). 74 | # The "public" dir that oranda made above will become the root dir 75 | # of this branch. 76 | # 77 | # Note that once the gh-pages branch exists, you must 78 | # go into repo's settings > pages and set "deploy from branch: gh-pages". 79 | # The other defaults work fine. 80 | - name: Deploy to Github Pages 81 | uses: JamesIves/github-pages-deploy-action@v4.4.1 82 | # ONLY if we're on main (so no PRs or feature branches allowed!) 83 | if: ${{ github.ref == 'refs/heads/main' }} 84 | with: 85 | branch: gh-pages 86 | # Gotta tell the action where to find oranda's output 87 | folder: public 88 | token: ${{ secrets.GITHUB_TOKEN }} 89 | single-commit: true -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::path::Path; 5 | 6 | use cdrom::cue::cd::CD; 7 | use cdrom::cue::track::{Track, TrackMode}; 8 | use cdrom::Disc; 9 | use cdrom::DiscProtection; 10 | use clap::{Parser, ValueEnum}; 11 | use miette::{Diagnostic, Result}; 12 | use thiserror::Error; 13 | 14 | #[derive(Error, Debug, Diagnostic)] 15 | enum Cue2CCDError { 16 | #[error("Couldn't find one or more files specified in the cuesheet.")] 17 | #[diagnostic(help("Missing files: {}", missing_files.join(", ")))] 18 | MissingFilesError { missing_files: Vec }, 19 | 20 | #[error("Unable to determine the directory {filename} is in!")] 21 | NoParentError { filename: String }, 22 | 23 | #[error("Unable to determine the filename portion of {filename}!")] 24 | NoFilenameError { filename: String }, 25 | 26 | // Thrown if SBI file exists but doesn't have the correct SBI header 27 | #[error("Invalid SBI file!")] 28 | InvalidSBIError {}, 29 | 30 | #[error("LSD does not match specified protection!")] 31 | InvalidProtectionLSDError {}, 32 | 33 | #[error("SBI does not match specified protection!")] 34 | InvalidProtectionSBIError {}, 35 | 36 | #[error("This tool only supports raw disc images")] 37 | #[diagnostic(help("cuesheets containing .wav files are not compatible."))] 38 | WaveFile {}, 39 | 40 | #[error("This tool only supports raw disc images")] 41 | #[diagnostic(help("cuesheets containing ISOs or other non-raw data are not compatible."))] 42 | CookedData {}, 43 | 44 | #[error(transparent)] 45 | IO(#[from] std::io::Error), 46 | 47 | #[error(transparent)] 48 | Cue(#[from] std::ffi::NulError), 49 | } 50 | 51 | #[derive(Clone, Debug, ValueEnum)] 52 | enum ProtectionType { 53 | #[clap(name = "discguard")] 54 | DiscGuard, 55 | } 56 | 57 | #[derive(Parser, Debug)] 58 | #[command( 59 | author, 60 | version, 61 | about, 62 | long_about = "Generate CCD and SUB files from BIN/CUE" 63 | )] 64 | struct Args { 65 | filename: String, 66 | #[arg(long, default_value_t = false)] 67 | skip_img_copy: bool, 68 | #[arg(long)] 69 | output_path: Option, 70 | #[arg(long, value_enum)] 71 | pub protection_type: Option, 72 | } 73 | 74 | fn validate_mode(tracks: &[Track]) -> Result<(), Cue2CCDError> { 75 | for track in tracks { 76 | if track.get_filename().ends_with(".wav") { 77 | return Err(Cue2CCDError::WaveFile {}); 78 | } 79 | match track.get_mode() { 80 | TrackMode::Mode1 | TrackMode::Mode2 | TrackMode::Mode2Form1 | TrackMode::Mode2Form2 => { 81 | return Err(Cue2CCDError::CookedData {}); 82 | } 83 | _ => (), 84 | } 85 | } 86 | Ok(()) 87 | } 88 | 89 | /// Fetches unique tracks from the list of tracks. 90 | /// If the same track appears multiple times in a row, 91 | /// returns only a single copy. 92 | fn get_unique_tracks(tracks: &[Track]) -> Vec { 93 | let mut files = vec![]; 94 | 95 | for track in tracks.iter() { 96 | let filename = track.get_filename(); 97 | if files.last() == Some(&filename) { 98 | continue; 99 | } 100 | files.push(filename); 101 | } 102 | 103 | files 104 | } 105 | 106 | // LSD File Format: 107 | // The file consists of subQ data, specifically consisting of the actual AMSF that the current subQ 108 | // was read from, followed by all 12 bytes of subQ data. LSD is definitively better as a file 109 | // format for storing subchannel data discrepancies as opposed to SBI, which forces you to 110 | // generate the CRC16 yourself (something that is a huge problem for SecuROM and LibCrypt if 111 | // you're aiming for accuracy) and ideally should always be preferred if possible. 112 | fn generate_lsd_data(raw_lsd_data: Vec) -> Result>, Cue2CCDError> { 113 | // LSD files have never been defined in the cuesheet, and programs (mainly just PS1 114 | // emulators so far) that make use of them simply check if there's an LSD file with the 115 | // same basename next to the .cue. If one exists, they use it, otherwise they don't. 116 | // It seems best to keep in line with this behavior 117 | 118 | let mut hash_map: HashMap> = HashMap::new(); 119 | // should always be multiple of 15 120 | for chunk in raw_lsd_data.chunks(15) { 121 | let mut q = vec![0; 12]; 122 | // These don't really need to be muts, but, they should always be getting set in the 123 | // enumeration, and it makes things easier to not have to pass them as options 124 | let mut m: i64 = 0; 125 | let mut s: i64 = 0; 126 | let mut f: i64 = 0; 127 | for (byte_index, &item) in chunk.iter().enumerate() { 128 | match byte_index { 129 | 0 => m = item as i64, 130 | 1 => s = item as i64, 131 | 2 => f = item as i64, 132 | _ => q[byte_index - 3] = item, 133 | } 134 | } 135 | hash_map.insert(cdrom::amsf_to_asec(m, s, f), q); 136 | } 137 | Ok(hash_map) 138 | } 139 | 140 | // SBI File Format: 141 | // Starts with header 0x53 0x42 0x49 0x00 ('S' 'B' 'I' '0x00') 142 | // The entire rest of the file consists of subQ data, specifically consisting of the actual 143 | // AMSF that the current subQ was read from, followed by a dummy 0x01 byte, followed by the first 144 | // 10 bytes of that subQ (so, everything but the CRC16). The exclusion of the CRC16 is obviously 145 | // annoying, *especially* for SecuROM and LibCrypt. LSD is a better file format, but at the 146 | // moment, redump will only generate LSD files for PS1 discs, and we do not have the power to 147 | // change the website; so, until a successor website exists, SBI support is necessary. It's 148 | // also still preferred by a lot of people and emulators for PS1 for some reason, despite 149 | // being worse than LSD. 150 | fn generate_sbi_data(raw_sbi_data: Vec) -> Result>, Cue2CCDError> { 151 | // SBI files have never been defined in the cuesheet, and programs (mainly just PS1 152 | // emulators so far) that make use of them simply check if there's an SBI file with the 153 | // same basename next to the .cue. If one exists, they use it, otherwise they don't. 154 | // It seems best to keep in line with this behavior 155 | 156 | let (header, data) = raw_sbi_data.split_at(4); 157 | let mut hash_map: HashMap> = HashMap::new(); 158 | if header != [83, 66, 73, 00] { 159 | // Checks for required [S][B][I][0x00] header 160 | return Err(Cue2CCDError::InvalidSBIError {}); 161 | } 162 | // should always be multiple of 14 163 | for chunk in data.chunks(14) { 164 | let mut q = vec![0; 10]; 165 | // These don't really need to be muts, but, they should always be getting set in the 166 | // enumeration, and it makes things easier to not have to pass them as options 167 | let mut m: i64 = 0; 168 | let mut s: i64 = 0; 169 | let mut f: i64 = 0; 170 | for (byte_index, &item) in chunk.iter().enumerate() { 171 | match byte_index { 172 | 0 => m = item as i64, 173 | 1 => s = item as i64, 174 | 2 => f = item as i64, 175 | // Index 3 excluded to ignore dummy 0x01 byte 176 | 3 => (), 177 | _ => q[byte_index - 4] = item, 178 | } 179 | } 180 | // Unlike LSD, SBI is missing the CRC16, so we have to do that 181 | // ourselves. 182 | let crc = cdrom::crc16(&q, cdrom::CRC16_INITIAL_CRC); 183 | q.push(((crc >> 8) & 0xFF) as u8); 184 | q.push((crc & 0xFF) as u8); 185 | 186 | hash_map.insert(cdrom::amsf_to_asec(m, s, f), q); 187 | } 188 | Ok(hash_map) 189 | } 190 | 191 | fn main() -> Result<(), miette::Report> { 192 | work()?; 193 | Ok(()) 194 | } 195 | 196 | fn work() -> Result<(), Cue2CCDError> { 197 | let args = Args::parse(); 198 | 199 | let Some(root) = Path::new(&args.filename).parent() else { 200 | return Err(Cue2CCDError::NoParentError { 201 | filename: args.filename, 202 | }); 203 | }; 204 | let Some(basename) = Path::new(&args.filename).file_name() else { 205 | return Err(Cue2CCDError::NoFilenameError { 206 | filename: args.filename, 207 | }); 208 | }; 209 | let path; 210 | let output_path; 211 | if let Some(p) = args.output_path { 212 | path = p; 213 | output_path = Path::new(&path); 214 | } else { 215 | output_path = root; 216 | } 217 | // Provides a pattern to build output filenames from 218 | let output_stem = output_path.join(basename); 219 | 220 | let cue_sheet = std::fs::read_to_string(&args.filename)?; 221 | 222 | let cd = CD::parse(cue_sheet)?; 223 | 224 | let tracks = cd.tracks(); 225 | 226 | // We validate that the track modes are compatible. BIN/CUE can be 227 | // a variety of different formats, including WAVE files and "cooked" 228 | // tracks with no error correction metadata. We need all raw files in 229 | // order to be able to merge into a CloneCD image. 230 | // In the future, it may be nice to support actually converting tracks 231 | // into the supported format, but right now that's out of scope. 232 | validate_mode(&tracks)?; 233 | 234 | let files = get_unique_tracks(&tracks); 235 | let missing_files = files 236 | .iter() 237 | .filter(|f| !root.join(f).is_file()) 238 | .cloned() 239 | .collect::>(); 240 | if !missing_files.is_empty() { 241 | return Err(Cue2CCDError::MissingFilesError { missing_files }); 242 | } 243 | let mut preconstructed_q_subcodes: HashMap> = Default::default(); 244 | 245 | let mut chosen_protection_type: Option = None; 246 | // TODO: #1 - see about making lsd/sbi extension checks not case sensitive 247 | // TODO: #2 - verify expected SBI/LSD sizes? 248 | // TODO: #3 - choose protection based off of lsd/sbi size if lsd/sbi is present and a 249 | // TODO: protection wasn't chosen? This can't be done universally, but it can be done for a 250 | // TODO: lot of stuff. That could also be an issue for anyone who wants to provide an LSD/SBI 251 | // TODO: for a non-protection related reason and happens to hit one of the exact sizes/contents 252 | // TODO: needed, but that is a use case that does not currently exist. 253 | if Path::new(&output_stem.with_extension("lsd")).exists() { 254 | // LSD files are very small, so it seems best to read the whole thing in first? 255 | let temp_hashmap = generate_lsd_data(std::fs::read(Path::new( 256 | &output_stem.with_extension("lsd"), 257 | ))?)?; 258 | let len = temp_hashmap.len(); 259 | if len == 76 { 260 | chosen_protection_type = Some(DiscProtection::DiscGuardScheme2); 261 | } else if len == 600 { 262 | chosen_protection_type = Some(DiscProtection::DiscGuardScheme1); 263 | } else if matches!(args.protection_type, Some(ProtectionType::DiscGuard)) { 264 | return Err(Cue2CCDError::InvalidProtectionLSDError {}); 265 | } 266 | preconstructed_q_subcodes = temp_hashmap; 267 | } else if Path::new(&output_stem.with_extension("sbi")).exists() { 268 | // SBI files are very small, so it seems best to read the whole thing in first? 269 | let temp_hashmap = generate_sbi_data(std::fs::read(Path::new( 270 | &output_stem.with_extension("sbi"), 271 | ))?)?; 272 | let len = temp_hashmap.len(); 273 | if len == 76 { 274 | chosen_protection_type = Some(DiscProtection::DiscGuardScheme2); 275 | } else if len == 600 { 276 | chosen_protection_type = Some(DiscProtection::DiscGuardScheme1); 277 | } else if matches!(args.protection_type, Some(ProtectionType::DiscGuard)) { 278 | return Err(Cue2CCDError::InvalidProtectionSBIError {}); 279 | } 280 | preconstructed_q_subcodes = temp_hashmap; 281 | } else if matches!(args.protection_type, Some(ProtectionType::DiscGuard)) { 282 | chosen_protection_type = Some(DiscProtection::DiscGuardScheme2); 283 | } 284 | 285 | let sub_target = output_stem.with_extension("sub"); 286 | let mut sub_write = File::create(sub_target)?; 287 | 288 | let disc = Disc::from_cuesheet(cd, root); 289 | for sector in disc.sectors() { 290 | sub_write.write_all( 291 | §or.generate_subchannel(&chosen_protection_type, &preconstructed_q_subcodes), 292 | )?; 293 | } 294 | 295 | let ccd_target = output_stem.with_extension("ccd"); 296 | let mut ccd_write = File::create(ccd_target)?; 297 | disc.write_ccd(&mut ccd_write)?; 298 | 299 | if !args.skip_img_copy { 300 | let img_target = output_stem.with_extension("img"); 301 | if img_target.exists() { 302 | eprintln!( 303 | "A .img file at path {} already exists; skipping copy", 304 | img_target.as_path().display() 305 | ); 306 | } else { 307 | let mut out_file = std::fs::OpenOptions::new() 308 | .create(true) 309 | .append(true) 310 | .open(&img_target)?; 311 | for fname in files { 312 | let mut in_file = File::open(root.join(&fname))?; 313 | std::io::copy(&mut in_file, &mut out_file)?; 314 | out_file.flush()?; 315 | } 316 | } 317 | } 318 | 319 | eprintln!( 320 | "Conversion complete! Created {}", 321 | output_stem.with_extension("ccd").display() 322 | ); 323 | 324 | Ok(()) 325 | } 326 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-22.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | persist-credentials: false 62 | submodules: recursive 63 | - name: Install dist 64 | # we specify bash to get pipefail; it guards against the `curl` command 65 | # failing. otherwise `sh` won't catch that `curl` returned non-0 66 | shell: bash 67 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh" 68 | - name: Cache dist 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: cargo-dist-cache 72 | path: ~/.cargo/bin/dist 73 | # sure would be cool if github gave us proper conditionals... 74 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 75 | # functionality based on whether this is a pull_request, and whether it's from a fork. 76 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 77 | # but also really annoying to build CI around when it needs secrets to work right.) 78 | - id: plan 79 | run: | 80 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 81 | echo "dist ran successfully" 82 | cat plan-dist-manifest.json 83 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 84 | - name: "Upload dist-manifest.json" 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: artifacts-plan-dist-manifest 88 | path: plan-dist-manifest.json 89 | 90 | # Build and packages all the platform-specific things 91 | build-local-artifacts: 92 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 93 | # Let the initial task tell us to not run (currently very blunt) 94 | needs: 95 | - plan 96 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 97 | strategy: 98 | fail-fast: false 99 | # Target platforms/runners are computed by dist in create-release. 100 | # Each member of the matrix has the following arguments: 101 | # 102 | # - runner: the github runner 103 | # - dist-args: cli flags to pass to dist 104 | # - install-dist: expression to run to install dist on the runner 105 | # 106 | # Typically there will be: 107 | # - 1 "global" task that builds universal installers 108 | # - N "local" tasks that build each platform's binaries and platform-specific installers 109 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 110 | runs-on: ${{ matrix.runner }} 111 | container: ${{ matrix.container && matrix.container.image || null }} 112 | env: 113 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 115 | steps: 116 | - name: enable windows longpaths 117 | run: | 118 | git config --global core.longpaths true 119 | - uses: actions/checkout@v4 120 | with: 121 | persist-credentials: false 122 | submodules: recursive 123 | - name: Install Rust non-interactively if not already installed 124 | if: ${{ matrix.container }} 125 | run: | 126 | if ! command -v cargo > /dev/null 2>&1; then 127 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 128 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 129 | fi 130 | - name: Install dist 131 | run: ${{ matrix.install_dist.run }} 132 | # Get the dist-manifest 133 | - name: Fetch local artifacts 134 | uses: actions/download-artifact@v4 135 | with: 136 | pattern: artifacts-* 137 | path: target/distrib/ 138 | merge-multiple: true 139 | - name: Install dependencies 140 | run: | 141 | ${{ matrix.packages_install }} 142 | - name: Build artifacts 143 | run: | 144 | # Actually do builds and make zips and whatnot 145 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 146 | echo "dist ran successfully" 147 | - id: cargo-dist 148 | name: Post-build 149 | # We force bash here just because github makes it really hard to get values up 150 | # to "real" actions without writing to env-vars, and writing to env-vars has 151 | # inconsistent syntax between shell and powershell. 152 | shell: bash 153 | run: | 154 | # Parse out what we just built and upload it to scratch storage 155 | echo "paths<> "$GITHUB_OUTPUT" 156 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 157 | echo "EOF" >> "$GITHUB_OUTPUT" 158 | 159 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 160 | - name: "Upload artifacts" 161 | uses: actions/upload-artifact@v4 162 | with: 163 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 164 | path: | 165 | ${{ steps.cargo-dist.outputs.paths }} 166 | ${{ env.BUILD_MANIFEST_NAME }} 167 | 168 | # Build and package all the platform-agnostic(ish) things 169 | build-global-artifacts: 170 | needs: 171 | - plan 172 | - build-local-artifacts 173 | runs-on: "ubuntu-22.04" 174 | env: 175 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 176 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 177 | steps: 178 | - uses: actions/checkout@v4 179 | with: 180 | persist-credentials: false 181 | submodules: recursive 182 | - name: Install cached dist 183 | uses: actions/download-artifact@v4 184 | with: 185 | name: cargo-dist-cache 186 | path: ~/.cargo/bin/ 187 | - run: chmod +x ~/.cargo/bin/dist 188 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 189 | - name: Fetch local artifacts 190 | uses: actions/download-artifact@v4 191 | with: 192 | pattern: artifacts-* 193 | path: target/distrib/ 194 | merge-multiple: true 195 | - id: cargo-dist 196 | shell: bash 197 | run: | 198 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 199 | echo "dist ran successfully" 200 | 201 | # Parse out what we just built and upload it to scratch storage 202 | echo "paths<> "$GITHUB_OUTPUT" 203 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 204 | echo "EOF" >> "$GITHUB_OUTPUT" 205 | 206 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 207 | - name: "Upload artifacts" 208 | uses: actions/upload-artifact@v4 209 | with: 210 | name: artifacts-build-global 211 | path: | 212 | ${{ steps.cargo-dist.outputs.paths }} 213 | ${{ env.BUILD_MANIFEST_NAME }} 214 | # Determines if we should publish/announce 215 | host: 216 | needs: 217 | - plan 218 | - build-local-artifacts 219 | - build-global-artifacts 220 | # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) 221 | if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 222 | env: 223 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 224 | runs-on: "ubuntu-22.04" 225 | outputs: 226 | val: ${{ steps.host.outputs.manifest }} 227 | steps: 228 | - uses: actions/checkout@v4 229 | with: 230 | persist-credentials: false 231 | submodules: recursive 232 | - name: Install cached dist 233 | uses: actions/download-artifact@v4 234 | with: 235 | name: cargo-dist-cache 236 | path: ~/.cargo/bin/ 237 | - run: chmod +x ~/.cargo/bin/dist 238 | # Fetch artifacts from scratch-storage 239 | - name: Fetch artifacts 240 | uses: actions/download-artifact@v4 241 | with: 242 | pattern: artifacts-* 243 | path: target/distrib/ 244 | merge-multiple: true 245 | - id: host 246 | shell: bash 247 | run: | 248 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 249 | echo "artifacts uploaded and released successfully" 250 | cat dist-manifest.json 251 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 252 | - name: "Upload dist-manifest.json" 253 | uses: actions/upload-artifact@v4 254 | with: 255 | # Overwrite the previous copy 256 | name: artifacts-dist-manifest 257 | path: dist-manifest.json 258 | # Create a GitHub Release while uploading all files to it 259 | - name: "Download GitHub Artifacts" 260 | uses: actions/download-artifact@v4 261 | with: 262 | pattern: artifacts-* 263 | path: artifacts 264 | merge-multiple: true 265 | - name: Cleanup 266 | run: | 267 | # Remove the granular manifests 268 | rm -f artifacts/*-dist-manifest.json 269 | - name: Create GitHub Release 270 | env: 271 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 272 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 273 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 274 | RELEASE_COMMIT: "${{ github.sha }}" 275 | run: | 276 | # Write and read notes from a file to avoid quoting breaking things 277 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 278 | 279 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 280 | 281 | publish-homebrew-formula: 282 | needs: 283 | - plan 284 | - host 285 | runs-on: "ubuntu-22.04" 286 | env: 287 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 288 | PLAN: ${{ needs.plan.outputs.val }} 289 | GITHUB_USER: "axo bot" 290 | GITHUB_EMAIL: "admin+bot@axo.dev" 291 | if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} 292 | steps: 293 | - uses: actions/checkout@v4 294 | with: 295 | persist-credentials: true 296 | repository: "mistydemeo/homebrew-formulae" 297 | token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 298 | # So we have access to the formula 299 | - name: Fetch homebrew formulae 300 | uses: actions/download-artifact@v4 301 | with: 302 | pattern: artifacts-* 303 | path: Formula/ 304 | merge-multiple: true 305 | # This is extra complex because you can make your Formula name not match your app name 306 | # so we need to find releases with a *.rb file, and publish with that filename. 307 | - name: Commit formula files 308 | run: | 309 | git config --global user.name "${GITHUB_USER}" 310 | git config --global user.email "${GITHUB_EMAIL}" 311 | 312 | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do 313 | filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) 314 | name=$(echo "$filename" | sed "s/\.rb$//") 315 | version=$(echo "$release" | jq .app_version --raw-output) 316 | 317 | export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" 318 | brew update 319 | # We avoid reformatting user-provided data such as the app description and homepage. 320 | brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true 321 | 322 | git add "Formula/${filename}" 323 | git commit -m "${name} ${version}" 324 | done 325 | git push 326 | 327 | announce: 328 | needs: 329 | - plan 330 | - host 331 | - publish-homebrew-formula 332 | # use "always() && ..." to allow us to wait for all publish jobs while 333 | # still allowing individual publish jobs to skip themselves (for prereleases). 334 | # "host" however must run to completion, no skipping allowed! 335 | if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} 336 | runs-on: "ubuntu-22.04" 337 | env: 338 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 339 | steps: 340 | - uses: actions/checkout@v4 341 | with: 342 | persist-credentials: false 343 | submodules: recursive 344 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.20.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is-terminal", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "1.0.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys", 67 | ] 68 | 69 | [[package]] 70 | name = "backtrace" 71 | version = "0.3.68" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" 74 | dependencies = [ 75 | "addr2line", 76 | "cc", 77 | "cfg-if", 78 | "libc", 79 | "miniz_oxide", 80 | "object", 81 | "rustc-demangle", 82 | ] 83 | 84 | [[package]] 85 | name = "backtrace-ext" 86 | version = "0.2.1" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" 89 | dependencies = [ 90 | "backtrace", 91 | ] 92 | 93 | [[package]] 94 | name = "bitflags" 95 | version = "1.3.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 98 | 99 | [[package]] 100 | name = "cc" 101 | version = "1.2.1" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" 104 | dependencies = [ 105 | "shlex", 106 | ] 107 | 108 | [[package]] 109 | name = "cdrom" 110 | version = "0.3.0" 111 | dependencies = [ 112 | "cdrom_crc", 113 | "cue", 114 | ] 115 | 116 | [[package]] 117 | name = "cdrom_crc" 118 | version = "0.1.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "b79de1ea79db6578ee25a7bcc8fddc58f958e6063aed54f426907d9d9f97895e" 121 | 122 | [[package]] 123 | name = "cfg-if" 124 | version = "1.0.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 127 | 128 | [[package]] 129 | name = "clap" 130 | version = "4.3.4" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" 133 | dependencies = [ 134 | "clap_builder", 135 | "clap_derive", 136 | "once_cell", 137 | ] 138 | 139 | [[package]] 140 | name = "clap_builder" 141 | version = "4.3.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" 144 | dependencies = [ 145 | "anstream", 146 | "anstyle", 147 | "bitflags", 148 | "clap_lex", 149 | "strsim", 150 | ] 151 | 152 | [[package]] 153 | name = "clap_derive" 154 | version = "4.3.2" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" 157 | dependencies = [ 158 | "heck", 159 | "proc-macro2", 160 | "quote", 161 | "syn", 162 | ] 163 | 164 | [[package]] 165 | name = "clap_lex" 166 | version = "0.5.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 169 | 170 | [[package]] 171 | name = "cmake" 172 | version = "0.1.51" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" 175 | dependencies = [ 176 | "cc", 177 | ] 178 | 179 | [[package]] 180 | name = "colorchoice" 181 | version = "1.0.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 184 | 185 | [[package]] 186 | name = "cue" 187 | version = "3.0.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "690eedca1eb72586e5359d32c43e8e7497aeed1c469b8b07b4eb8500056d3287" 190 | dependencies = [ 191 | "cue-sys", 192 | "libc", 193 | ] 194 | 195 | [[package]] 196 | name = "cue-sys" 197 | version = "2.0.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "aacab32605a00e807f2fe1f8c5ecfab91da08d34269bcd7319ca60c41a4a806f" 200 | dependencies = [ 201 | "cmake", 202 | "libc", 203 | ] 204 | 205 | [[package]] 206 | name = "cue2ccd" 207 | version = "1.1.0" 208 | dependencies = [ 209 | "cdrom", 210 | "clap", 211 | "miette", 212 | "thiserror", 213 | ] 214 | 215 | [[package]] 216 | name = "errno" 217 | version = "0.3.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 220 | dependencies = [ 221 | "errno-dragonfly", 222 | "libc", 223 | "windows-sys", 224 | ] 225 | 226 | [[package]] 227 | name = "errno-dragonfly" 228 | version = "0.1.2" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 231 | dependencies = [ 232 | "cc", 233 | "libc", 234 | ] 235 | 236 | [[package]] 237 | name = "gimli" 238 | version = "0.27.3" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" 241 | 242 | [[package]] 243 | name = "heck" 244 | version = "0.4.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 247 | 248 | [[package]] 249 | name = "hermit-abi" 250 | version = "0.3.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 253 | 254 | [[package]] 255 | name = "io-lifetimes" 256 | version = "1.0.11" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 259 | dependencies = [ 260 | "hermit-abi", 261 | "libc", 262 | "windows-sys", 263 | ] 264 | 265 | [[package]] 266 | name = "is-terminal" 267 | version = "0.4.7" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 270 | dependencies = [ 271 | "hermit-abi", 272 | "io-lifetimes", 273 | "rustix", 274 | "windows-sys", 275 | ] 276 | 277 | [[package]] 278 | name = "is_ci" 279 | version = "1.1.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" 282 | 283 | [[package]] 284 | name = "libc" 285 | version = "0.2.147" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 288 | 289 | [[package]] 290 | name = "linux-raw-sys" 291 | version = "0.3.8" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 294 | 295 | [[package]] 296 | name = "memchr" 297 | version = "2.5.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 300 | 301 | [[package]] 302 | name = "miette" 303 | version = "5.10.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" 306 | dependencies = [ 307 | "backtrace", 308 | "backtrace-ext", 309 | "is-terminal", 310 | "miette-derive", 311 | "once_cell", 312 | "owo-colors", 313 | "supports-color", 314 | "supports-hyperlinks", 315 | "supports-unicode", 316 | "terminal_size", 317 | "textwrap", 318 | "thiserror", 319 | "unicode-width", 320 | ] 321 | 322 | [[package]] 323 | name = "miette-derive" 324 | version = "5.10.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" 327 | dependencies = [ 328 | "proc-macro2", 329 | "quote", 330 | "syn", 331 | ] 332 | 333 | [[package]] 334 | name = "miniz_oxide" 335 | version = "0.7.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 338 | dependencies = [ 339 | "adler", 340 | ] 341 | 342 | [[package]] 343 | name = "object" 344 | version = "0.31.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" 347 | dependencies = [ 348 | "memchr", 349 | ] 350 | 351 | [[package]] 352 | name = "once_cell" 353 | version = "1.18.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 356 | 357 | [[package]] 358 | name = "owo-colors" 359 | version = "3.5.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 362 | 363 | [[package]] 364 | name = "proc-macro2" 365 | version = "1.0.66" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 368 | dependencies = [ 369 | "unicode-ident", 370 | ] 371 | 372 | [[package]] 373 | name = "quote" 374 | version = "1.0.32" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 377 | dependencies = [ 378 | "proc-macro2", 379 | ] 380 | 381 | [[package]] 382 | name = "rustc-demangle" 383 | version = "0.1.23" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 386 | 387 | [[package]] 388 | name = "rustix" 389 | version = "0.37.20" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" 392 | dependencies = [ 393 | "bitflags", 394 | "errno", 395 | "io-lifetimes", 396 | "libc", 397 | "linux-raw-sys", 398 | "windows-sys", 399 | ] 400 | 401 | [[package]] 402 | name = "shlex" 403 | version = "1.3.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 406 | 407 | [[package]] 408 | name = "smawk" 409 | version = "0.3.1" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" 412 | 413 | [[package]] 414 | name = "strsim" 415 | version = "0.10.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 418 | 419 | [[package]] 420 | name = "supports-color" 421 | version = "2.0.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "4950e7174bffabe99455511c39707310e7e9b440364a2fcb1cc21521be57b354" 424 | dependencies = [ 425 | "is-terminal", 426 | "is_ci", 427 | ] 428 | 429 | [[package]] 430 | name = "supports-hyperlinks" 431 | version = "2.1.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" 434 | dependencies = [ 435 | "is-terminal", 436 | ] 437 | 438 | [[package]] 439 | name = "supports-unicode" 440 | version = "2.0.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" 443 | dependencies = [ 444 | "is-terminal", 445 | ] 446 | 447 | [[package]] 448 | name = "syn" 449 | version = "2.0.28" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 452 | dependencies = [ 453 | "proc-macro2", 454 | "quote", 455 | "unicode-ident", 456 | ] 457 | 458 | [[package]] 459 | name = "terminal_size" 460 | version = "0.1.17" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 463 | dependencies = [ 464 | "libc", 465 | "winapi", 466 | ] 467 | 468 | [[package]] 469 | name = "textwrap" 470 | version = "0.15.2" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" 473 | dependencies = [ 474 | "smawk", 475 | "unicode-linebreak", 476 | "unicode-width", 477 | ] 478 | 479 | [[package]] 480 | name = "thiserror" 481 | version = "1.0.44" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" 484 | dependencies = [ 485 | "thiserror-impl", 486 | ] 487 | 488 | [[package]] 489 | name = "thiserror-impl" 490 | version = "1.0.44" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" 493 | dependencies = [ 494 | "proc-macro2", 495 | "quote", 496 | "syn", 497 | ] 498 | 499 | [[package]] 500 | name = "unicode-ident" 501 | version = "1.0.9" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 504 | 505 | [[package]] 506 | name = "unicode-linebreak" 507 | version = "0.1.5" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 510 | 511 | [[package]] 512 | name = "unicode-width" 513 | version = "0.1.10" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 516 | 517 | [[package]] 518 | name = "utf8parse" 519 | version = "0.2.1" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 522 | 523 | [[package]] 524 | name = "winapi" 525 | version = "0.3.9" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 528 | dependencies = [ 529 | "winapi-i686-pc-windows-gnu", 530 | "winapi-x86_64-pc-windows-gnu", 531 | ] 532 | 533 | [[package]] 534 | name = "winapi-i686-pc-windows-gnu" 535 | version = "0.4.0" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 538 | 539 | [[package]] 540 | name = "winapi-x86_64-pc-windows-gnu" 541 | version = "0.4.0" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 544 | 545 | [[package]] 546 | name = "windows-sys" 547 | version = "0.48.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 550 | dependencies = [ 551 | "windows-targets", 552 | ] 553 | 554 | [[package]] 555 | name = "windows-targets" 556 | version = "0.48.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 559 | dependencies = [ 560 | "windows_aarch64_gnullvm", 561 | "windows_aarch64_msvc", 562 | "windows_i686_gnu", 563 | "windows_i686_msvc", 564 | "windows_x86_64_gnu", 565 | "windows_x86_64_gnullvm", 566 | "windows_x86_64_msvc", 567 | ] 568 | 569 | [[package]] 570 | name = "windows_aarch64_gnullvm" 571 | version = "0.48.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 574 | 575 | [[package]] 576 | name = "windows_aarch64_msvc" 577 | version = "0.48.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 580 | 581 | [[package]] 582 | name = "windows_i686_gnu" 583 | version = "0.48.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 586 | 587 | [[package]] 588 | name = "windows_i686_msvc" 589 | version = "0.48.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 592 | 593 | [[package]] 594 | name = "windows_x86_64_gnu" 595 | version = "0.48.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 598 | 599 | [[package]] 600 | name = "windows_x86_64_gnullvm" 601 | version = "0.48.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 604 | 605 | [[package]] 606 | name = "windows_x86_64_msvc" 607 | version = "0.48.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 610 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /cdrom/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::{self, Write}; 4 | use std::path::Path; 5 | 6 | pub use cdrom_crc::{crc16, CRC16_INITIAL_CRC}; 7 | pub use cue; 8 | use cue::cd::CD; 9 | use cue::track; 10 | 11 | fn lba_to_msf(lba: i64) -> (i64, i64, i64) { 12 | (lba / 4500, (lba / 75) % 60, lba % 75) 13 | } 14 | 15 | // Converts Absolute MSF to Absolute Sector number 16 | pub fn amsf_to_asec(m: i64, s: i64, f: i64) -> i64 { 17 | let mut absolute_sector: i64 = 0; 18 | absolute_sector += 4500 * (((m / 16) * 10) + (m % 16)); 19 | absolute_sector += 75 * (((s / 16) * 10) + (s % 16)); 20 | absolute_sector += ((f / 16) * 10) + (f % 16); 21 | 22 | absolute_sector 23 | } 24 | 25 | pub struct Disc { 26 | pub tracks: Vec, 27 | pub sector_count: i64, 28 | } 29 | 30 | impl Disc { 31 | pub fn sectors(&self) -> SectorIterator<'_> { 32 | SectorIterator { 33 | current: 0, 34 | disc: self, 35 | } 36 | } 37 | 38 | pub fn write_ccd(&self, writer: &mut File) -> io::Result<()> { 39 | write!(writer, "{}", self.generate_ccd()) 40 | } 41 | 42 | pub fn generate_ccd(&self) -> String { 43 | let mut result = String::new(); 44 | 45 | // Instead of using a real INI parser, write out via format strings. 46 | // The stuff we're doing here is simple enough. 47 | // Note that many values here are hardcoded, because we're not doing a 48 | // full implementation of every CD feature, even if they were in the 49 | // source BIN/CUE. 50 | result.push_str("[CloneCD]\n"); 51 | result.push_str("Version=3\n\n"); 52 | 53 | result.push_str("[Disc]\n"); 54 | // We always write out exactly 3 TOC entries more than the number of tracks. 55 | // That accounts for extra TOC entries such as the leadout. 56 | result.push_str(format!("TocEntries={}\n", self.tracks.len() + 3).as_str()); 57 | // Multisession cuesheets are rare, we're pretending they don't exist 58 | result.push_str("Sessions=1\n"); 59 | result.push_str("DataTracksScrambled=0\n"); 60 | // CD-TEXT not yet supported 61 | result.push_str("CDTextLength=0\n\n"); 62 | 63 | // To match other tools, we write track 1 and the final track before 64 | // going back to write the other tracks. 65 | let first_track = &self.tracks[0]; 66 | let last_track = if self.tracks.len() > 1 { 67 | &self.tracks[self.tracks.len() - 1] 68 | } else { 69 | first_track 70 | }; 71 | 72 | result.push_str("[Session 1]\n"); 73 | // Appears to be the type of the first track; 74 | // even in a mixed-mode disc, this is only specified once. 75 | // Is it possible for this to differ from the type of the first track? Unclear. 76 | result.push_str(format!("PreGapMode={}\n", first_track.mode.as_u8()).as_str()); 77 | // Appears to be subchannel for pregap according to Aaru: 78 | // https://github.com/aaru-dps/Aaru/blob/5410ae5e74f2177887cd1e0e1866d8d55cf244d9/Aaru.Images/CloneCD/Constants.cs#L50 79 | // Unclear what the "correct" value is, but safe to hardcode. 80 | result.push_str("PreGapSubC=0\n\n"); 81 | 82 | let mut entry = 0; 83 | 84 | result.push_str( 85 | self.generate_track(entry, Pointer::FirstTrack, first_track) 86 | .as_str(), 87 | ); 88 | entry += 1; 89 | result.push_str( 90 | self.generate_track(entry, Pointer::LastTrack, last_track) 91 | .as_str(), 92 | ); 93 | entry += 1; 94 | result.push_str( 95 | self.generate_track(entry, Pointer::LeadOut, last_track) 96 | .as_str(), 97 | ); 98 | entry += 1; 99 | 100 | for track in &self.tracks { 101 | result.push_str( 102 | self.generate_track(entry, Pointer::Track(track.number), track) 103 | .as_str(), 104 | ); 105 | entry += 1; 106 | } 107 | 108 | // Next, we want to handle writing out the track index. 109 | // This is a vaguely cuesheet-like format that's optional. 110 | for track in &self.tracks { 111 | result.push_str(self.generate_track_entry(track).as_str()); 112 | } 113 | 114 | result 115 | } 116 | 117 | fn generate_track(&self, entry: usize, pointer: Pointer, track: &Track) -> String { 118 | let mut result = String::new(); 119 | // The data in a CCD file is a low-level representation of the disc's leadin 120 | // in a plaintext INI format. 121 | // For some more information keys and their values, see 122 | // https://psx-spx.consoledev.net/cdromdrive/ 123 | result.push_str(format!("[Entry {}]\n", entry).as_str()); 124 | result.push_str("Session=1\n"); 125 | // Pointer is either a track number from 1 to 99, *or* it's a control 126 | // code. Valid control codes according to the spec are: 127 | // A0 - P-MIN field indicates the first information track, and P-SEC/P-FRAC are zero 128 | // A1 - P-MIN field indicates the last information track, and P-SEC/P-FRAC are zero 129 | // A2 - P-MIN field indicates the start of the leadout, and P-SEC/P-FRAC are zero 130 | // For more detail, see section 22.3.4.2 of ECMA-130. 131 | result.push_str(format!("Point=0x{:02x}\n", pointer.as_u8()).as_str()); 132 | 133 | // Next, based on that value, we need to determine how to set M/S/F. 134 | // They might not actually be the real timekeeping info, based on the above. 135 | let lba; 136 | let m; 137 | let s; 138 | let f; 139 | match pointer { 140 | Pointer::FirstTrack | Pointer::LastTrack => { 141 | lba = track.number as i64 * 4500 - 150; 142 | m = track.number as i64; 143 | s = 0; 144 | f = 0; 145 | } 146 | Pointer::LeadOut => { 147 | lba = self.sector_count; 148 | // M/S/F is absolute, counting the 150 lead-in sectors, 149 | // while the LBA is relative to the start of disc content. 150 | (m, s, f) = lba_to_msf(lba + 150); 151 | } 152 | _ => { 153 | lba = track.start; 154 | (m, s, f) = lba_to_msf(track.start + 150); 155 | } 156 | } 157 | 158 | result.push_str("ADR=0x01\n"); 159 | // Control field. This is a 4-bit value defining the track type. 160 | // There are more settings, but we only set these two. 161 | // See section 22.3.1 of ECMA-130. 162 | // TODO: Ensure this control code is correct for leadin and leadout. 163 | // One real disc had 0 for the leadin when the first track was data, 164 | // while other discs use 4. 4 is *probably* safe. 165 | let control = if let TrackMode::Audio = track.mode { 166 | // Audio track, all bits 0 167 | 0 168 | } else { 169 | // Data with copy flag set - 0100 170 | 4 171 | }; 172 | result.push_str(format!("Control=0x{:02x}\n", control).as_str()); 173 | // Yes, this is hardcodable despite what it looks like 174 | result.push_str("TrackNo=0\n"); 175 | // Despite the A-MIN/SEC/FRAC values in the subchannel always containing 176 | // an absolute timestamp, here they're always zeroed out. 177 | result.push_str("AMin=0\n"); 178 | result.push_str("ASec=0\n"); 179 | result.push_str("AFrame=0\n"); 180 | // Should probably be calculated based on the pregap 181 | result.push_str("ALBA=-150\n"); 182 | result.push_str("Zero=0\n"); 183 | // These three next values are the absolute MIN/SEC/FRAC 184 | result.push_str(format!("PMin={}\n", m).as_str()); 185 | result.push_str(format!("PSec={}\n", s).as_str()); 186 | result.push_str(format!("PFrame={}\n", f).as_str()); 187 | result.push_str(format!("PLBA={}\n\n", lba).as_str()); 188 | 189 | result 190 | } 191 | 192 | fn generate_track_entry(&self, track: &Track) -> String { 193 | let mut result = String::new(); 194 | 195 | result.push_str(format!("[TRACK {}]\n", track.number).as_str()); 196 | result.push_str(format!("MODE={}\n", track.mode.as_u8()).as_str()); 197 | 198 | for index in &track.indices { 199 | result.push_str(format!("INDEX {}={}\n", index.number, index.start).as_str()); 200 | } 201 | 202 | result 203 | } 204 | } 205 | 206 | pub struct SectorIterator<'a> { 207 | current: i64, 208 | disc: &'a Disc, 209 | } 210 | 211 | impl<'a> SectorIterator<'a> { 212 | pub fn sector_from_number(&self, sector: i64) -> Option { 213 | // We should start at or around sector 0 (actually 150, but who's counting) 214 | // (me, I am), which means we can iterate through tracks and indices in order 215 | // safely until we hit the one that starts at our sector. 216 | for track in &self.disc.tracks { 217 | for (i, index) in track.indices.iter().enumerate() { 218 | // Edge of the index is either the start of the next index (if there's 219 | // another index) or the end of the track. 220 | let boundary = if let Some(next) = track.indices.get(i + 1) { 221 | next.start - 1 222 | } else { 223 | track.start + track.length 224 | }; 225 | 226 | if index.start <= sector && boundary >= sector { 227 | // Yes, it's okay for this to be negative! Pregap counts backwards 228 | // to the start of the following index. 229 | let relative_position = sector - track.start; 230 | 231 | return Some(Sector { 232 | start: sector, 233 | // Convenience for indexing relative to the start of the disc, 234 | // rather than the start of the disc image. 235 | // Yes, it means the first sector isn't sector 1. 236 | absolute_start: sector + 150, 237 | relative_position, 238 | size: 2352, // TODO un-hardcode this 239 | // Worry about lifetimes later, this is small anyway 240 | track: track.clone(), 241 | index: index.clone(), 242 | }); 243 | } 244 | } 245 | } 246 | 247 | None 248 | } 249 | } 250 | 251 | impl<'a> Iterator for SectorIterator<'a> { 252 | type Item = Sector; 253 | 254 | fn next(&mut self) -> Option { 255 | if self.current >= self.disc.sector_count { 256 | return None; 257 | } 258 | 259 | let sector = self.sector_from_number(self.current); 260 | 261 | self.current += 1; 262 | 263 | sector 264 | } 265 | } 266 | 267 | fn sector_length(path: &Path) -> i64 { 268 | let metadata = match path.metadata() { 269 | Ok(m) => m, 270 | Err(_) => return 0, 271 | }; 272 | 273 | metadata.len() as i64 / 2352 274 | } 275 | 276 | impl Disc { 277 | pub fn from_cuesheet(cuesheet: CD, root: &Path) -> Disc { 278 | let mut previous_file: Option = None; 279 | let mut disc_length_so_far = 0; 280 | let mut current_track_length = 0; 281 | 282 | let mut tracks = vec![]; 283 | for (i, track) in cuesheet.tracks().iter().enumerate() { 284 | let current_file = track.get_filename(); 285 | current_track_length = sector_length(&root.join(¤t_file)); 286 | 287 | // At the start of a new file, track the offset 288 | if let Some(previous) = &previous_file { 289 | if previous != ¤t_file { 290 | disc_length_so_far += sector_length(&root.join(previous)); 291 | } 292 | } 293 | 294 | let tracknum = i as u8 + 1; 295 | 296 | let start = track.get_start() + disc_length_so_far; 297 | // The last track on the disc will have indeterminate length, 298 | // because the cuesheet doesn't store that; we need to calculate 299 | // it from the size of the current disc/track image. 300 | let length = track 301 | .get_length() 302 | .unwrap_or(disc_length_so_far + current_track_length - start); 303 | 304 | let mut indices = vec![]; 305 | for i in 0..99 { 306 | if let Some(index) = track.get_index(i) { 307 | let index = index + disc_length_so_far as isize; 308 | // Cuesheet doesn't actually track the end of an index, 309 | // so we need to either calculate the boundary of the next 310 | // index within the track or the end of the track itself. 311 | let end = if let Some(next) = track.get_index(i + 1) { 312 | next as i64 - 1 + start 313 | } else { 314 | start + track.get_length().unwrap_or(current_track_length) 315 | }; 316 | 317 | indices.push(Index { 318 | number: i as u8, 319 | start: index as i64, 320 | end, 321 | }); 322 | } 323 | } 324 | 325 | tracks.push(Track { 326 | number: tracknum, 327 | start, 328 | length, 329 | indices, 330 | mode: TrackMode::from_cue_mode(track.get_mode()), 331 | }); 332 | 333 | if previous_file != Some(current_file.to_string()) { 334 | previous_file = Some(current_file.to_string()); 335 | } 336 | } 337 | 338 | Disc { 339 | tracks, 340 | sector_count: disc_length_so_far + current_track_length, 341 | } 342 | } 343 | } 344 | 345 | #[derive(Clone, Debug)] 346 | pub struct Track { 347 | pub number: u8, 348 | pub start: i64, 349 | pub length: i64, 350 | pub indices: Vec, 351 | pub mode: TrackMode, 352 | } 353 | 354 | // Ugly workaround to avoid embedding cue types, rework later 355 | #[derive(Clone, Copy, Debug)] 356 | pub enum TrackMode { 357 | Audio, 358 | /// 2048-byte data without ECC 359 | Mode1, 360 | /// 2048-byte data with ECC 361 | Mode1Raw, 362 | /// 2336-byte data without ECC 363 | Mode2, 364 | /// 2048-byte data (CD-ROM XA) 365 | Mode2Form1, 366 | /// 2324-byte data (CD-ROM XA) 367 | Mode2Form2, 368 | /// 2332-byte data (CD-ROM XA) 369 | Mode2FormMix, 370 | /// 2336-byte data with ECC 371 | Mode2Raw, 372 | } 373 | 374 | impl TrackMode { 375 | fn from_cue_mode(mode: track::TrackMode) -> TrackMode { 376 | match mode { 377 | track::TrackMode::Audio => TrackMode::Audio, 378 | track::TrackMode::Mode1 => TrackMode::Mode1, 379 | track::TrackMode::Mode1Raw => TrackMode::Mode1Raw, 380 | track::TrackMode::Mode2 => TrackMode::Mode2, 381 | track::TrackMode::Mode2Form1 => TrackMode::Mode2Form1, 382 | track::TrackMode::Mode2Form2 => TrackMode::Mode2Form2, 383 | track::TrackMode::Mode2FormMix => TrackMode::Mode2FormMix, 384 | track::TrackMode::Mode2Raw => TrackMode::Mode2Raw, 385 | } 386 | } 387 | 388 | pub fn as_u8(&self) -> u8 { 389 | match self { 390 | TrackMode::Audio => 0, 391 | TrackMode::Mode1 | TrackMode::Mode1Raw => 1, 392 | TrackMode::Mode2 393 | | TrackMode::Mode2Raw 394 | | TrackMode::Mode2Form1 395 | | TrackMode::Mode2Form2 396 | | TrackMode::Mode2FormMix => 2, 397 | } 398 | } 399 | } 400 | 401 | #[derive(Clone, Debug)] 402 | pub struct Index { 403 | // Number of the current index; index 0 is the pregap, index 1 onward are the track proper 404 | pub number: u8, 405 | // Start of the current index, in sectors 406 | pub start: i64, 407 | // End of the current index, in sectors 408 | pub end: i64, 409 | } 410 | 411 | #[derive(Debug)] 412 | pub struct Sector { 413 | // Sector number, relative to the start of the image 414 | pub start: i64, 415 | // Sector number, relative to the start of the disc 416 | pub absolute_start: i64, 417 | // Relative position to index 1 of the current track 418 | pub relative_position: i64, 419 | // Size of the sector, in bytes 420 | pub size: usize, 421 | // Metadata for the current track 422 | pub track: Track, 423 | // Metadata for the current index 424 | pub index: Index, 425 | } 426 | 427 | fn bcd(dec: i64) -> u8 { 428 | (((dec / 10) << 4) | (dec % 10)) as u8 429 | } 430 | 431 | impl Sector { 432 | // The subchannel data contains extra sidecar metadata required to read 433 | // the disc, but which isn't a part of the data itself. 434 | // Some applications can read CloneCD data with zeroed out subchannel data 435 | // provided that the more verbose form of the CCD control file is used, 436 | // but other applications require real subchannel data no matter what. 437 | // 438 | // The CloneCD format stores the subchannel data in a sidecar file, which 439 | // is essentially identical to the data on the disc with a few exceptions: 440 | // 1) The leadin (the first 150 sectors) is omitted, and so is the 441 | // table of contents that's usually stored there. 442 | // 2) The first two subchannel bytes, which contain sync words, are omitted. 443 | // 3) The subchannel data is unrolled into eight sequential sections of 444 | // 12 bytes instead of interleaved bits. This is easier to read and write 445 | // in non-streaming applications. 446 | // In total, we need to write 96 bytes of subchannel data for each sector. 447 | // 448 | // More information is in ECMA-130: 449 | // http://www.ecma-international.org/publications/standards/Ecma-130.htm 450 | pub fn generate_subchannel( 451 | &self, 452 | chosen_protection_type: &Option, 453 | preconstructed_q_subchannels: &HashMap>, 454 | ) -> Vec { 455 | // The first sector of a track, and only the first sector, 456 | // gets an FFed out P sector like a pregap. Every other non-pregap 457 | // sector uses 0s. (Section 22.2) 458 | // For players which ignore the Q subchannel, this allows 459 | // locating the start of tracks. 460 | let mut p = if self.relative_position == 0 || self.index.number == 0 { 461 | vec![0xFF; 12] 462 | } else { 463 | vec![0; 12] 464 | }; 465 | let mut q = Sector::generate_q_subchannel( 466 | self.absolute_start, 467 | self.relative_position, 468 | self.track.number, 469 | self.index.number, 470 | self.track.mode, 471 | chosen_protection_type, 472 | preconstructed_q_subchannels, 473 | ); 474 | // The vast majority of real discs write their unused R-W fields as 0s, 475 | // but at least one real disc used FFs instead. We'll side with the 476 | // majority and use 0. 477 | let mut rest = vec![0; 72]; 478 | 479 | let mut out = vec![]; 480 | out.append(&mut p); 481 | out.append(&mut q); 482 | out.append(&mut rest); 483 | 484 | out 485 | } 486 | 487 | fn generate_q_subchannel( 488 | absolute_sector: i64, 489 | relative_sector: i64, 490 | track: u8, 491 | index: u8, 492 | track_type: TrackMode, 493 | chosen_protection_type: &Option, 494 | preconstructed_q_subchannels: &HashMap>, 495 | ) -> Vec { 496 | // LSD/SBI checked without checking for specific protection chosen because technically 497 | // speaking, there's no reason you *shouldn't* be able to provide an LSD/SBI file even if 498 | // you didn't choose protection 499 | 500 | if let Some(q) = preconstructed_q_subchannels.get(&absolute_sector) { 501 | q.clone() 502 | } else { 503 | Sector::generate_q_subchannel_from_scratch( 504 | absolute_sector, 505 | relative_sector, 506 | track, 507 | index, 508 | track_type, 509 | chosen_protection_type, 510 | ) 511 | } 512 | } 513 | 514 | fn generate_q_subchannel_from_scratch( 515 | absolute_sector: i64, 516 | relative_sector: i64, 517 | track: u8, 518 | index: u8, 519 | track_type: TrackMode, 520 | chosen_protection_type: &Option, 521 | ) -> Vec { 522 | // This channel made up of a sequence of bits; we'll start by 523 | // zeroing it out, then setting individual bits. 524 | let mut q = vec![0; 12]; 525 | 526 | // First four bits are the control field. 527 | // We only care about setting the data bit, 1; the others are 528 | // irrelevant for this application. 529 | match track_type { 530 | TrackMode::Audio => (), 531 | _ => q[0] |= 1 << 6, 532 | }; 533 | 534 | // Next four bits indicate the mode of the Q channel. 535 | // There are three modes: 536 | // * 1 - Table of contents (used during the lead-in) 537 | // * 2 - Media Catalog Number 538 | // * 3 - International Standard Recording Code (ISRC) 539 | // In practice, we're always generating mode 1 540 | // every sector so we'll hardcode this. 541 | // Note that the cuesheet *can* contain the catalog number, 542 | // so it'd be possible for us to set this, but libcue doesn't 543 | // expose a getter for that; it's simpler just to skip it. 544 | q[0] |= 1 << 0; 545 | // OK, it's data time! This is the next 9 bytes. 546 | // This contains timing info for the current track. 547 | q[1] = bcd(track as i64); 548 | 549 | // Next is the index. While it supports values up to 99, 550 | // usually only two values are seen: 551 | // 00 - Pregap or postgap 552 | // 01 - First index within the track, or leadout 553 | match chosen_protection_type { 554 | // For some reason, for later/main variant DiscGuard discs, index 02 is only applied for 555 | // the q subchannel in sectors 450-525. Probably not important, but I'd like to be 556 | // accurate. 557 | Some(DiscProtection::DiscGuardScheme2) => { 558 | if relative_sector > 525 && track == 1 { 559 | q[2] = bcd(1); 560 | } else { 561 | q[2] = bcd(index as i64); 562 | } 563 | } 564 | Some(DiscProtection::DiscGuardScheme1) => { 565 | if relative_sector >= 33075 && track == 1 { 566 | q[2] = bcd(1); 567 | } else { 568 | q[2] = bcd(index as i64); 569 | } 570 | } 571 | _ => { 572 | q[2] = bcd(index as i64); 573 | } //SecuROM and LibCrypt currently not implemented 574 | } 575 | 576 | // The next three fields, MIN, SEC, and FRAC, are the 577 | // running time within each index. 578 | // FRAC is a unit of 1/75th of a second, e.g. the 579 | // duration of exactly one sector. 580 | // In the pregap, this starts at negative the 581 | // pregap duration and counts up to 0. 582 | // In the actual content, this starts at 0 and 583 | // counts up. 584 | // 585 | // Since bcd doesn't represent negative numbers, we 586 | // re-negate this; we start at the pregap duration and 587 | // count down to 0. 588 | let relative_sector_count = if 0 > relative_sector { 589 | 0 - relative_sector 590 | } else { 591 | relative_sector 592 | }; 593 | // MIN 594 | q[3] = bcd(relative_sector_count / 4500); 595 | // SEC 596 | match chosen_protection_type { 597 | // Nice for convenience, but it wouldn't be unreasonable to remove 598 | // this with the expectation that the user should be providing their own 599 | // LSD/SBI anyways. Keeping for now, just putting this here for anyone who 600 | // may want to consider it in the future. 601 | 602 | // I don't worry about checking for LSD or SBI here since if one was provided, it 603 | // will never reach this code anyways. 604 | Some(DiscProtection::DiscGuardScheme2) => { 605 | if (675..=750).contains(&relative_sector) { 606 | q[4] = bcd(29); 607 | } else { 608 | q[4] = bcd((relative_sector_count / 75) % 60); 609 | } 610 | } 611 | _ => { 612 | q[4] = bcd((relative_sector_count / 75) % 60); 613 | } //SecuROM and LibCrypt currently not implemented 614 | } 615 | // FRAC 616 | q[5] = bcd(relative_sector_count % 75); 617 | // Next byte is always zero 618 | q[6] = 0; 619 | // The next three bytes provide an absolute timestamp, 620 | // rather than a timestamp within the current track. 621 | // These three fields, A-MIN, A-SEC, and A-FRAC, are 622 | // stored the same way as the relative timestamps. 623 | q[7] = bcd(absolute_sector / 4500); 624 | q[8] = bcd((absolute_sector / 75) % 60); 625 | q[9] = bcd(absolute_sector % 75); 626 | // The last two bytes contain a CRC of the main data. 627 | let crc = crc16(&q[0..10], CRC16_INITIAL_CRC); 628 | q[10] = ((crc >> 8) & 0xFF) as u8; 629 | q[11] = (crc & 0xFF) as u8; 630 | 631 | q 632 | } 633 | } 634 | 635 | //TODO: Possible protections, improve descriptions after review 636 | #[derive(Debug)] 637 | pub enum DiscProtection { 638 | DiscGuardScheme1, 639 | /// Change one second of sector MSFs 640 | DiscGuardScheme2, 641 | /// Subchannel-error-based PC protection 642 | SecuROMScheme1, 643 | SecuROMScheme2, 644 | SecuROMScheme3a, 645 | SecuROMScheme3b, 646 | SecuROMScheme3c, 647 | SecuROMScheme4, 648 | /// Subchannel-error-based PS1 protection 649 | LibCryptScheme1, 650 | LibCryptScheme2, 651 | } 652 | 653 | // For more detail, see section 22.3.4.2 of ECMA-130. 654 | enum Pointer { 655 | Track(u8), 656 | FirstTrack, 657 | LastTrack, 658 | LeadOut, 659 | } 660 | 661 | impl Pointer { 662 | fn as_u8(&self) -> u8 { 663 | match self { 664 | Self::Track(value) => *value, 665 | Self::FirstTrack => 0xA0, 666 | Self::LastTrack => 0xA1, 667 | Self::LeadOut => 0xA2, 668 | } 669 | } 670 | } 671 | 672 | #[cfg(test)] 673 | mod tests { 674 | use cue::cd::CD; 675 | use std::collections::HashMap; 676 | use std::fs::{read_to_string, File}; 677 | use std::io::Read; 678 | use std::{io::Write, path::PathBuf}; 679 | 680 | use crate::Disc; 681 | 682 | #[derive(Debug)] 683 | struct TestPaths { 684 | data_plus_audio_cue: PathBuf, 685 | data_plus_audio_ccd: PathBuf, 686 | one_track_cue: PathBuf, 687 | one_track_ccd: PathBuf, 688 | } 689 | 690 | fn get_test_paths() -> TestPaths { 691 | let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 692 | .join("..") 693 | .join("testdata"); 694 | 695 | TestPaths { 696 | data_plus_audio_cue: root.join("dataplusaudio").join("bincue"), 697 | data_plus_audio_ccd: root.join("dataplusaudio").join("ccd"), 698 | one_track_cue: root.join("onetrack").join("bincue"), 699 | one_track_ccd: root.join("onetrack").join("ccd"), 700 | } 701 | } 702 | 703 | #[test] 704 | fn test_onetrack_subchannel() { 705 | let paths = get_test_paths(); 706 | let in_cue = paths.one_track_cue.join("basic_image.cue"); 707 | let cue_sheet = read_to_string(&in_cue).unwrap(); 708 | 709 | let cd = CD::parse(cue_sheet).unwrap(); 710 | let disc = Disc::from_cuesheet(cd, &paths.one_track_cue); 711 | 712 | let mut buf = vec![]; 713 | for sector in disc.sectors() { 714 | buf.write_all(§or.generate_subchannel(&None, &HashMap::new())) 715 | .unwrap(); 716 | } 717 | 718 | let real_sub_path = paths.one_track_ccd.join("basic_image.sub"); 719 | let mut real_sub_file = File::open(real_sub_path).unwrap(); 720 | let mut real_sub = vec![]; 721 | real_sub_file.read_to_end(&mut real_sub).unwrap(); 722 | 723 | assert_eq!(real_sub, buf); 724 | } 725 | 726 | #[test] 727 | fn test_onetrack_ccd() { 728 | let paths = get_test_paths(); 729 | let in_cue = paths.one_track_cue.join("basic_image.cue"); 730 | let cue_sheet = read_to_string(in_cue).unwrap(); 731 | 732 | let cd = CD::parse(cue_sheet).unwrap(); 733 | let disc = Disc::from_cuesheet(cd, &paths.one_track_cue); 734 | 735 | let ccd = disc.generate_ccd(); 736 | 737 | let real_ccd_path = paths.one_track_ccd.join("basic_image.ccd"); 738 | let real_ccd = read_to_string(real_ccd_path).unwrap(); 739 | 740 | assert_eq!(real_ccd, ccd); 741 | } 742 | 743 | #[test] 744 | fn test_multitrack_subchannel() { 745 | let paths = get_test_paths(); 746 | let in_cue = paths.data_plus_audio_cue.join("disc.cue"); 747 | let cue_sheet = read_to_string(&in_cue).unwrap(); 748 | 749 | let cd = CD::parse(cue_sheet).unwrap(); 750 | let disc = Disc::from_cuesheet(cd, &paths.data_plus_audio_cue); 751 | 752 | let mut buf = vec![]; 753 | for sector in disc.sectors() { 754 | buf.write_all(§or.generate_subchannel(&None, &HashMap::new())) 755 | .unwrap(); 756 | } 757 | 758 | let real_sub_path = paths.data_plus_audio_ccd.join("disc.sub"); 759 | let mut real_sub_file = File::open(real_sub_path).unwrap(); 760 | let mut real_sub = vec![]; 761 | real_sub_file.read_to_end(&mut real_sub).unwrap(); 762 | 763 | assert_eq!(real_sub, buf); 764 | } 765 | 766 | #[test] 767 | fn test_multitrack_ccd() { 768 | let paths = get_test_paths(); 769 | let in_cue = paths.data_plus_audio_cue.join("disc.cue"); 770 | let cue_sheet = read_to_string(in_cue).unwrap(); 771 | 772 | let cd = CD::parse(cue_sheet).unwrap(); 773 | let disc = Disc::from_cuesheet(cd, &paths.data_plus_audio_cue); 774 | 775 | let ccd = disc.generate_ccd(); 776 | 777 | let real_ccd_path = paths.data_plus_audio_ccd.join("disc.ccd"); 778 | let real_ccd = read_to_string(real_ccd_path).unwrap(); 779 | 780 | assert_eq!(real_ccd, ccd); 781 | } 782 | } 783 | --------------------------------------------------------------------------------