From 8852fe6c9096ce25400b3684fe632d3654b97938 Mon Sep 17 00:00:00 2001 From: Denis Avvakumov Date: Fri, 5 Jun 2026 09:08:29 +0300 Subject: [PATCH] Improve bundled Opus build and packet handling --- .github/workflows/ci.yml | 44 +- Cargo.lock | 488 +++++++++++++++-- Cargo.toml | 31 +- README.md | 6 + build.rs | 444 ++++++++++++++-- scripts/verify_windows_static_crt.py | 162 ++++++ src/constants.rs | 15 + src/decoder.rs | 21 +- src/dred.rs | 65 ++- src/encoder.rs | 4 + src/multistream.rs | 22 +- src/packet.rs | 104 +++- src/packet/layout.rs | 748 +++++++++++++++++++++++++++ src/packet/layout/extensions.rs | 683 ++++++++++++++++++++++++ src/projection.rs | 46 +- src/repacketizer.rs | 118 ++++- tests/encoder_decoder.rs | 7 + tests/multistream.rs | 24 + tests/packet_padding.rs | 304 +++++++++++ tests/repacketizer.rs | 474 ++++++++++++++++- 20 files changed, 3641 insertions(+), 169 deletions(-) create mode 100644 scripts/verify_windows_static_crt.py create mode 100644 src/packet/layout.rs create mode 100644 src/packet/layout/extensions.rs create mode 100644 tests/packet_padding.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c627743..8ec7cba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,8 @@ name: CI +permissions: + contents: read + on: push: branches: @@ -11,7 +14,7 @@ jobs: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: cargo fmt @@ -21,7 +24,7 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: cargo clippy @@ -31,7 +34,7 @@ jobs: name: Cargo Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: cargo check @@ -41,7 +44,7 @@ jobs: name: MSRV Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@1.87.0 - name: cargo check @@ -53,9 +56,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-2025-vs2026] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install ffmpeg (Ubuntu) @@ -67,18 +70,39 @@ jobs: if: matrix.os == 'macos-latest' run: brew install ffmpeg - name: Install ffmpeg (Windows) - if: matrix.os == 'windows-latest' + if: startsWith(matrix.os, 'windows-') shell: pwsh run: choco install ffmpeg --no-progress -y - name: cargo test run: cargo test + windows-static-crt: + name: Windows Static CRT + runs-on: windows-2025-vs2026 + env: + CARGO_TARGET_DIR: target/ci-windows-static-crt + CRT_VERIFY_TARGET: x86_64-pc-windows-msvc + RUSTFLAGS: -C target-feature=+crt-static + steps: + - uses: actions/checkout@v6 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + - name: Install ffmpeg + shell: pwsh + run: choco install ffmpeg --no-progress -y + - name: Verify static CRT imports + run: python scripts/verify_windows_static_crt.py + - name: cargo test (static CRT) + run: cargo test --target x86_64-pc-windows-msvc + avx-presume: name: AVX presume feature runs-on: ubuntu-latest needs: [fmt, clippy, check, tests] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Verify AVX presume gating @@ -89,7 +113,7 @@ jobs: runs-on: ubuntu-latest needs: [fmt, clippy, check, tests] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install build dependencies @@ -104,7 +128,7 @@ jobs: runs-on: ubuntu-latest needs: [fmt, clippy, check, tests] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install deps (ffmpeg + wget for model download) diff --git a/Cargo.lock b/Cargo.lock index f1f2e3d..9141faf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "bindgen" version = "0.72.1" @@ -27,24 +33,33 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn", ] [[package]] name = "bitflags" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "cc" -version = "1.2.50" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -62,6 +77,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -75,18 +101,62 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -100,26 +170,44 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core", "wasip2", + "wasip3", ] [[package]] @@ -128,6 +216,45 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + [[package]] name = "itertools" version = "0.13.0" @@ -137,11 +264,23 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.178" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -155,21 +294,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "minimal-lexical" @@ -189,25 +328,27 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opus-codec" -version = "0.1.2" +version = "0.2.0" dependencies = [ "bindgen", "cmake", "pkg-config", + "rand", + "sha2", "tempfile", ] [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "prettyplease" @@ -221,33 +362,50 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -257,9 +415,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -268,21 +426,21 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -291,17 +449,82 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -310,9 +533,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", @@ -321,19 +544,80 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "wit-bindgen", + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -353,6 +637,100 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index fb11bb0..c8fe194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opus-codec" -version = "0.1.2" +version = "0.2.0" edition = "2024" authors = ["Denis Avvakumov"] description = "Safe Rust bindings for the Opus audio codec" @@ -11,13 +11,37 @@ homepage = "https://github.com/Deniskore" keywords = ["audio", "codec", "compression", "opus", "voice"] categories = ["api-bindings", "encoding", "compression", "multimedia::audio"] readme = "README.md" +exclude = [ + "opus/opus_data-*.tar.gz", + "opus/dnn/models/**", + "opus/dnn/dred_rdovae_constants.h", + "opus/dnn/dred_rdovae_dec_data.c", + "opus/dnn/dred_rdovae_dec_data.h", + "opus/dnn/dred_rdovae_enc_data.c", + "opus/dnn/dred_rdovae_enc_data.h", + "opus/dnn/dred_rdovae_stats_data.c", + "opus/dnn/dred_rdovae_stats_data.h", + "opus/dnn/fargan_data.c", + "opus/dnn/fargan_data.h", + "opus/dnn/lace_data.c", + "opus/dnn/lace_data.h", + "opus/dnn/lossgen_data.c", + "opus/dnn/lossgen_data.h", + "opus/dnn/nolace_data.c", + "opus/dnn/nolace_data.h", + "opus/dnn/pitchdnn_data.c", + "opus/dnn/pitchdnn_data.h", + "opus/dnn/plc_data.c", + "opus/dnn/plc_data.h", +] [dependencies] [build-dependencies] cmake = { version = "0.1" } bindgen = "0.72.1" -pkg-config = "0.3" +pkg-config = "0.3.33" +sha2 = "0.10.9" [features] default = [] @@ -26,4 +50,5 @@ system-lib = [] presume-avx2 = [] [dev-dependencies] -tempfile = "3.23.0" +rand = "0.10.1" +tempfile = "3.27.0" diff --git a/README.md b/README.md index 82ef0bf..81d545b 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,9 @@ at your option. The upstream libopus sources are vendored via `git subtree` at tag **v1.5.2** (split commit `ddbe48383984d56acd9e1ab6a090c54ca6b735a6`). You can verify the copy is pristine by diffing `opus/` against that upstream commit. + +## Windows MSVC + +Bundled builds follow Cargo's selected C runtime automatically. By default, `opus-codec` builds the vendored `libopus` with the dynamic MSVC runtime. If you build with `RUSTFLAGS="-C target-feature=+crt-static"`, the bundled `libopus` build switches to the static MSVC runtime as well. + +If you enable the `system-lib` feature, `opus-codec` links against an already installed `libopus` instead of the vendored copy. In that case, the installed `libopus` must use the same CRT mode as the final binary. diff --git a/build.rs b/build.rs index 21c6d57..84f74a3 100644 --- a/build.rs +++ b/build.rs @@ -1,17 +1,25 @@ +use sha2::{Digest, Sha256}; +use std::borrow::Cow; use std::env; +use std::path::{Path, PathBuf}; -fn main() { - emit_rerun_directives(); - let opts = BuildOptions::from_env(); - - if opts.use_system_lib { - handle_system_lib(&opts); - } else { - build_bundled_and_link(&opts); - } - - generate_bindings(); -} +const BUNDLED_PACKET_OPS_FINGERPRINTS: &[SourceFingerprint] = &[ + SourceFingerprint { + path: "src/opus.c", + len: 10_051, + sha256: "f5ae5ff3e9cef998addeee777dcb283cffcaf0f6ee4452108127e9157cdb2458", + }, + SourceFingerprint { + path: "src/repacketizer.c", + len: 13_550, + sha256: "ad6df845cdcd4e8a61a43069f2ee6f34a9ae7fa27c534935ebeda0f4d1903fa3", + }, + SourceFingerprint { + path: "src/extensions.c", + len: 9_373, + sha256: "f1458c7d257400b025181dfae96a5ec02d9fc76566a9b8f1ea65714e3dbb3459", + }, +]; struct BuildOptions { use_system_lib: bool, @@ -19,6 +27,7 @@ struct BuildOptions { presume_avx: bool, target_arch: String, avx_allowed: bool, + msvc_runtime: Option, } impl BuildOptions { @@ -28,6 +37,7 @@ impl BuildOptions { let presume_avx = env::var("CARGO_FEATURE_PRESUME_AVX2").is_ok(); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let avx_allowed = presume_avx && matches!(target_arch.as_str(), "x86" | "x86_64"); + let msvc_runtime = MsvcRuntime::from_cargo(); Self { use_system_lib, @@ -35,20 +45,93 @@ impl BuildOptions { presume_avx, target_arch, avx_allowed, + msvc_runtime, + } + } +} + +#[derive(Clone, Copy)] +enum MsvcRuntime { + Dynamic, + Static, +} + +#[derive(Clone, Copy)] +struct PacketOpsCompatibility { + rust_packet_ops: bool, + frame_bounded_extensions: bool, +} + +#[derive(Clone, Copy)] +struct SourceFingerprint { + path: &'static str, + len: u64, + sha256: &'static str, +} + +impl MsvcRuntime { + fn from_cargo() -> Option { + if !target_is_windows_msvc() { + return None; + } + + let uses_static_runtime = target_feature_enabled("crt-static"); + + Some(if uses_static_runtime { + Self::Static + } else { + Self::Dynamic + }) + } + + fn is_static(self) -> bool { + matches!(self, Self::Static) + } + + fn opus_static_runtime(self) -> &'static str { + match self { + Self::Dynamic => "OFF", + Self::Static => "ON", } } } +fn main() { + emit_rerun_directives(); + let opts = BuildOptions::from_env(); + + if opts.use_system_lib { + println!("cargo:rustc-cfg=opus_codec_system_lib"); + } + + if opts.use_system_lib { + handle_system_lib(&opts); + } else { + build_bundled_and_link(&opts); + } + + generate_bindings(); +} + fn emit_rerun_directives() { + println!("cargo:rustc-check-cfg=cfg(opus_codec_system_lib)"); + println!("cargo:rustc-check-cfg=cfg(opus_codec_rust_packet_ops)"); + println!("cargo:rustc-check-cfg=cfg(opus_codec_frame_bounded_extensions)"); println!("cargo:rerun-if-changed=opus/include/opus.h"); println!("cargo:rerun-if-changed=opus/include/opus_defines.h"); println!("cargo:rerun-if-changed=opus/include/opus_types.h"); println!("cargo:rerun-if-changed=opus/include/opus_multistream.h"); println!("cargo:rerun-if-changed=opus/include/opus_projection.h"); + println!("cargo:rerun-if-changed=opus/src/opus.c"); + println!("cargo:rerun-if-changed=opus/src/repacketizer.c"); + println!("cargo:rerun-if-changed=opus/src/extensions.c"); println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=opus/dnn/download_model.sh"); + println!("cargo:rerun-if-changed=opus/opus_data-735117b.tar.gz"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_SYSTEM_LIB"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_PRESUME_AVX2"); + println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_ENV"); + println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_FAMILY"); + println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_FEATURE"); } fn handle_system_lib(opts: &BuildOptions) { @@ -62,13 +145,11 @@ fn handle_system_lib(opts: &BuildOptions) { "cargo:warning=presume-avx2 feature enabled; ensure the system libopus was built with OPUS_X86_PRESUME_AVX2" ); } - link_system_lib(); + let lib = link_system_lib(); + emit_system_libopus_cfg(&lib.version); } fn build_bundled_and_link(opts: &BuildOptions) { - if opts.dred_enabled { - ensure_dred_assets(); - } if opts.presume_avx && !opts.avx_allowed { println!( "cargo:warning=presume-avx2 feature only applies to x86/x86_64 targets; ignoring for {}", @@ -76,36 +157,42 @@ fn build_bundled_and_link(opts: &BuildOptions) { ); } - let dst = build_bundled(opts.dred_enabled, opts.avx_allowed); + let opus_source = bundled_opus_source(opts); + let dst = build_bundled(opts, &opus_source); + emit_bundled_libopus_cfg(&opus_source); println!("cargo:rustc-link-search=native={}/lib", dst.display()); + println!("cargo:rustc-link-search=native={}/lib64", dst.display()); println!("cargo:rustc-link-lib=static=opus"); } -fn build_bundled(dred_enabled: bool, presume_avx: bool) -> std::path::PathBuf { - let mut config = cmake::Config::new("opus"); +fn bundled_opus_source(opts: &BuildOptions) -> PathBuf { + if opts.dred_enabled { + prepare_dred_opus_source() + } else { + PathBuf::from("opus") + } +} + +fn build_bundled(opts: &BuildOptions, opus_source: &Path) -> std::path::PathBuf { + let mut config = cmake::Config::new(opus_source); config.profile("Release"); - if should_use_msvc_crt_flag() { - let profile = env::var("PROFILE").unwrap_or_default(); - let crt_flag = if profile.eq_ignore_ascii_case("debug") { - "/MDd" - } else { - "/MD" - }; - config.cflag(crt_flag); + if let Some(runtime) = opts.msvc_runtime { + config.static_crt(runtime.is_static()); + config.define("OPUS_STATIC_RUNTIME", runtime.opus_static_runtime()); } config .define("OPUS_BUILD_SHARED_LIBRARY", "OFF") .define("OPUS_BUILD_TESTING", "OFF") .define("OPUS_BUILD_PROGRAMS", "OFF") - .define("OPUS_DRED", if dred_enabled { "ON" } else { "OFF" }) + .define("OPUS_DRED", if opts.dred_enabled { "ON" } else { "OFF" }) .define("BUILD_SHARED_LIBS", "OFF") .define("OPUS_DISABLE_INTRINSICS", "OFF") .define("CMAKE_POSITION_INDEPENDENT_CODE", "ON"); - if presume_avx { + if opts.presume_avx { config .define("OPUS_X86_PRESUME_AVX2", "ON") .define("OPUS_X86_MAY_HAVE_AVX2", "ON"); @@ -114,11 +201,258 @@ fn build_bundled(dred_enabled: bool, presume_avx: bool) -> std::path::PathBuf { config.build() } -fn link_system_lib() { +fn link_system_lib() -> pkg_config::Library { pkg_config::Config::new() .atleast_version("1.5.2") .probe("opus") - .expect("system-lib feature requested but pkg-config couldn't find libopus"); + .expect("system-lib feature requested but pkg-config couldn't find libopus") +} + +fn emit_system_libopus_cfg(version: &str) { + match version { + "1.5.2" => emit_packet_ops_cfg(PacketOpsCompatibility { + rust_packet_ops: true, + frame_bounded_extensions: false, + }), + "1.6.1" => emit_packet_ops_cfg(PacketOpsCompatibility { + rust_packet_ops: true, + frame_bounded_extensions: true, + }), + _ => println!( + "cargo:warning=system libopus {version} is not one of the exact packet-op versions \ + supported by opus-codec (1.5.2, 1.6.1); packet padding and repacketizer emission \ + will delegate to the linked C libopus" + ), + } +} + +fn emit_bundled_libopus_cfg(opus_source: &Path) { + if bundled_packet_ops_match(opus_source) { + emit_packet_ops_cfg(PacketOpsCompatibility { + rust_packet_ops: true, + frame_bounded_extensions: false, + }); + } else { + println!( + "cargo:warning=vendored libopus packet-op sources do not match the audited \ + compatibility fingerprints; packet padding and repacketizer emission will \ + delegate to bundled C libopus" + ); + } +} + +fn emit_packet_ops_cfg(compatibility: PacketOpsCompatibility) { + if compatibility.rust_packet_ops { + emit_rust_packet_ops_cfg(); + } + if compatibility.frame_bounded_extensions { + emit_frame_bounded_extensions_cfg(); + } +} + +fn emit_rust_packet_ops_cfg() { + println!("cargo:rustc-cfg=opus_codec_rust_packet_ops"); +} + +fn emit_frame_bounded_extensions_cfg() { + println!("cargo:rustc-cfg=opus_codec_frame_bounded_extensions"); +} + +fn bundled_packet_ops_match(opus_source: &Path) -> bool { + BUNDLED_PACKET_OPS_FINGERPRINTS + .iter() + .all(|fingerprint| source_fingerprint_matches(opus_source, *fingerprint)) +} + +fn source_fingerprint_matches(opus_source: &Path, fingerprint: SourceFingerprint) -> bool { + let path = opus_source.join(fingerprint.path); + let bytes = std::fs::read(&path) + .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display())); + let bytes = normalize_source_line_endings(&bytes); + let actual_len = u64::try_from(bytes.len()).expect("source file length does not fit in u64"); + let actual_hash = sha256_hex_bytes(&bytes); + if actual_len == fingerprint.len && actual_hash == fingerprint.sha256 { + return true; + } + + println!( + "cargo:warning=vendored libopus packet-op source fingerprint mismatch for {}: \ + expected normalized len {}, sha256 {}; got normalized len {}, sha256 {}", + path.display(), + fingerprint.len, + fingerprint.sha256, + actual_len, + actual_hash + ); + false +} + +fn normalize_source_line_endings(bytes: &[u8]) -> Cow<'_, [u8]> { + if !bytes.windows(2).any(|window| window == b"\r\n") { + return Cow::Borrowed(bytes); + } + + let mut normalized = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') { + normalized.push(b'\n'); + index += 2; + } else { + normalized.push(bytes[index]); + index += 1; + } + } + Cow::Owned(normalized) +} + +fn prepare_dred_opus_source() -> PathBuf { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is not set by Cargo")); + let opus_source = out_dir.join("opus-dred-src"); + if opus_source.exists() { + std::fs::remove_dir_all(&opus_source) + .unwrap_or_else(|err| panic!("failed to remove {}: {err}", opus_source.display())); + } + copy_opus_source_tree(Path::new("opus"), &opus_source) + .unwrap_or_else(|err| panic!("failed to copy vendored opus source: {err}")); + ensure_dred_assets(&opus_source, &out_dir); + opus_source +} + +fn copy_opus_source_tree(src: &Path, dst: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dst)?; + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + if should_skip_dred_generated_path(&src_path) { + continue; + } + let metadata = entry.metadata()?; + if metadata.is_dir() { + copy_opus_source_tree(&src_path, &dst_path)?; + } else if metadata.is_file() { + std::fs::copy(&src_path, &dst_path)?; + } + } + Ok(()) +} + +fn should_skip_dred_generated_path(path: &Path) -> bool { + let Ok(rel) = path.strip_prefix("opus") else { + return false; + }; + let rel = rel.to_string_lossy().replace('\\', "/"); + matches!( + rel.as_str(), + "opus_data-735117b.tar.gz" + | "dnn/dred_rdovae_constants.h" + | "dnn/dred_rdovae_dec_data.c" + | "dnn/dred_rdovae_dec_data.h" + | "dnn/dred_rdovae_enc_data.c" + | "dnn/dred_rdovae_enc_data.h" + | "dnn/dred_rdovae_stats_data.c" + | "dnn/dred_rdovae_stats_data.h" + | "dnn/fargan_data.c" + | "dnn/fargan_data.h" + | "dnn/lace_data.c" + | "dnn/lace_data.h" + | "dnn/lossgen_data.c" + | "dnn/lossgen_data.h" + | "dnn/nolace_data.c" + | "dnn/nolace_data.h" + | "dnn/pitchdnn_data.c" + | "dnn/pitchdnn_data.h" + | "dnn/plc_data.c" + | "dnn/plc_data.h" + | "dnn/models" + ) || rel.starts_with("dnn/models/") +} + +fn ensure_dred_assets(opus_source: &Path, out_dir: &Path) { + use std::path::Component; + use std::process::Command; + + const MODEL_REV: &str = "735117b"; + const MODEL_ARCHIVE: &str = "opus_data-735117b.tar.gz"; + const MODEL_SHA256: &str = "8f34305a299183509d22c7ba66790f67916a0fc56028ebd4c8f7b938458f2801"; + const REQUIRED_FILE: &str = "dnn/fargan_data.h"; + if opus_source.join(REQUIRED_FILE).exists() { + return; + } + + let cached_archive_path = Path::new("opus").join(MODEL_ARCHIVE); + let archive_path = if cached_archive_path.exists() { + std::fs::canonicalize(&cached_archive_path).unwrap_or_else(|err| { + panic!( + "failed to canonicalize cached DRED archive {}: {err}", + cached_archive_path.display() + ) + }) + } else { + out_dir.join(MODEL_ARCHIVE) + }; + if !archive_path.exists() { + let status = Command::new("wget") + .arg("-O") + .arg(&archive_path) + .arg(format!( + "https://media.xiph.org/opus/models/opus_data-{MODEL_REV}.tar.gz" + )) + .status() + .expect("failed to spawn wget for DRED model download"); + + if !status.success() { + panic!("downloading DRED model assets failed (exit status: {status})"); + } + } + + let actual = sha256_hex(&archive_path); + if actual != MODEL_SHA256 { + panic!( + "DRED model archive checksum mismatch for {}: expected {}, got {}", + archive_path.display(), + MODEL_SHA256, + actual + ); + } + + let listing = Command::new("tar") + .arg("tf") + .arg(&archive_path) + .output() + .expect("failed to list DRED model archive"); + if !listing.status.success() { + panic!( + "listing DRED model archive failed (exit status: {})", + listing.status + ); + } + for entry in String::from_utf8_lossy(&listing.stdout).lines() { + let path = Path::new(entry); + if path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) { + panic!("DRED model archive contains unsafe path: {entry}"); + } + } + + let status = Command::new("tar") + .arg("xvomf") + .arg(&archive_path) + .current_dir(opus_source) + .status() + .expect("failed to extract DRED model archive"); + if !status.success() { + panic!("extracting DRED model assets failed (exit status: {status})"); + } + + if !opus_source.join(REQUIRED_FILE).exists() { + panic!("DRED model download completed but {REQUIRED_FILE} is still missing"); + } } fn generate_bindings() { @@ -146,39 +480,35 @@ fn generate_bindings() { .expect("Couldn't write bindings!"); } -fn should_use_msvc_crt_flag() -> bool { +fn target_is_windows_msvc() -> bool { matches!( env::var("CARGO_CFG_TARGET_FAMILY").as_deref(), Ok("windows") ) && matches!(env::var("CARGO_CFG_TARGET_ENV").as_deref(), Ok("msvc")) } -fn ensure_dred_assets() { - use std::path::Path; - use std::process::Command; - - const REQUIRED_FILE: &str = "opus/dnn/fargan_data.h"; - if Path::new(REQUIRED_FILE).exists() { - return; - } - - let script = Path::new("opus/dnn/download_model.sh"); - if !script.exists() { - panic!("DRED feature requires {script:?}, but it was not found"); +fn target_feature_enabled(feature_name: &str) -> bool { + match env::var("CARGO_CFG_TARGET_FEATURE") { + Ok(features) => features + .split(',') + .map(str::trim) + .any(|feature| feature == feature_name), + Err(_) => false, } +} - let status = Command::new("sh") - .arg("dnn/download_model.sh") - .arg("735117b") - .current_dir("opus") - .status() - .expect("failed to spawn DRED model download script"); - - if !status.success() { - panic!("downloading DRED model assets failed (exit status: {status})"); - } +fn sha256_hex(path: &Path) -> String { + let bytes = std::fs::read(path) + .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display())); + sha256_hex_bytes(&bytes) +} - if !Path::new(REQUIRED_FILE).exists() { - panic!("DRED model download completed but {REQUIRED_FILE} is still missing"); +fn sha256_hex_bytes(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut hex = String::with_capacity(digest.len() * 2); + for byte in digest { + use std::fmt::Write as _; + write!(&mut hex, "{byte:02x}").expect("writing to String should not fail"); } + hex } diff --git a/scripts/verify_windows_static_crt.py b/scripts/verify_windows_static_crt.py new file mode 100644 index 0000000..417f90f --- /dev/null +++ b/scripts/verify_windows_static_crt.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +Build Windows static-CRT test executables and verify they do not import +dynamic CRT DLLs. +""" + +import json +import os +import shutil +from pathlib import Path +from typing import Iterable, List + +import ci_utils + + +TARGET = os.environ.get("CRT_VERIFY_TARGET", "x86_64-pc-windows-msvc") +TARGET_DIR = Path(os.environ.get("CARGO_TARGET_DIR", "target/ci-windows-static-crt")) +FORBIDDEN_DLL_PREFIXES = ( + "api-ms-win-crt-", + "msvcp", + "msvcr", + "ucrtbase", + "vcruntime", +) + + +def find_dumpbin() -> Path: + dumpbin = shutil.which("dumpbin") + if dumpbin: + return Path(dumpbin) + + program_files_x86 = Path( + os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)") + ) + vswhere = ( + program_files_x86 + / "Microsoft Visual Studio" + / "Installer" + / "vswhere.exe" + ) + if not vswhere.exists(): + ci_utils.fail("vswhere.exe not found and dumpbin.exe is not on PATH") + + install_path = ci_utils.run( + [ + str(vswhere), + "-latest", + "-products", + "*", + "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", + "installationPath", + ], + capture_output=True, + ).stdout.strip() + if not install_path: + ci_utils.fail("Could not locate a Visual Studio installation with VC tools") + + candidates = sorted( + Path(install_path).glob("VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe") + ) + if not candidates: + ci_utils.fail("dumpbin.exe not found inside the Visual Studio installation") + return candidates[-1] + + +def build_test_executables() -> List[Path]: + result = ci_utils.run( + [ + "cargo", + "test", + "--no-run", + "--message-format=json", + "--target", + TARGET, + "--target-dir", + str(TARGET_DIR), + ], + capture_output=True, + ) + + target_root = (TARGET_DIR / TARGET).resolve() + executables: List[Path] = [] + for line in result.stdout.splitlines(): + line = line.strip() + if not line.startswith("{"): + continue + message = json.loads(line) + if message.get("reason") != "compiler-artifact": + continue + + executable = message.get("executable") + if not executable: + continue + + path = Path(executable).resolve() + if path.suffix.lower() != ".exe": + continue + if target_root not in path.parents: + continue + executables.append(path) + + deduped = list(dict.fromkeys(executables)) + if not deduped: + ci_utils.fail("No target test executables were produced for import inspection") + return deduped + + +def imported_dlls(dumpbin: Path, executable: Path) -> List[str]: + output = ci_utils.run( + [str(dumpbin), "/dependents", str(executable)], + capture_output=True, + ).stdout + imports = [] + for line in output.splitlines(): + candidate = line.strip().lower() + if candidate.endswith(".dll"): + imports.append(candidate) + return imports + + +def forbidden_imports(imports: Iterable[str]) -> List[str]: + return sorted( + { + dll + for dll in imports + if dll.startswith(FORBIDDEN_DLL_PREFIXES) + } + ) + + +def main() -> None: + if os.name != "nt": + ci_utils.fail("verify_windows_static_crt.py must run on Windows") + + dumpbin = find_dumpbin() + print(f"Using dumpbin at: {dumpbin}") + + executables = build_test_executables() + print(f"Inspecting {len(executables)} test executable(s)") + + failures = [] + for executable in executables: + with ci_utils.group(f"Inspect {executable.name}"): + imports = imported_dlls(dumpbin, executable) + for dll in imports: + print(f" {dll}") + forbidden = forbidden_imports(imports) + if forbidden: + failures.append((executable, forbidden)) + + if failures: + for executable, forbidden in failures: + print(f"{executable} imports forbidden CRT DLLs: {', '.join(forbidden)}") + ci_utils.fail("Static CRT verification failed") + + print("Static CRT verification passed: no dynamic CRT DLL imports found.") + + +if __name__ == "__main__": + main() diff --git a/src/constants.rs b/src/constants.rs index 245c353..ab89f6a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -17,3 +17,18 @@ pub const fn max_frame_samples_for(sample_rate: SampleRate) -> usize { // sample_rate.as_i32() is always positive given valid SampleRate enum values (MAX_FRAME_SAMPLES_48KHZ * (sample_rate as usize)) / 48_000 } + +/// Number of samples per channel in a 2.5 ms frame at the given `sample_rate`. +/// +/// libopus requires PLC/FEC and DRED frame sizes to be multiples of this value. +#[must_use] +pub const fn samples_per_2_5ms(sample_rate: SampleRate) -> usize { + (sample_rate as usize) / 400 +} + +/// Returns `true` when `frame_size` is a multiple of 2.5 ms at `sample_rate`. +#[must_use] +pub const fn is_frame_size_2_5ms_aligned(frame_size: usize, sample_rate: SampleRate) -> bool { + let quant = samples_per_2_5ms(sample_rate); + quant > 0 && frame_size.is_multiple_of(quant) +} diff --git a/src/decoder.rs b/src/decoder.rs index 3fb4e83..0f4068c 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -12,7 +12,7 @@ use crate::bindings::{ opus_decoder_create, opus_decoder_ctl, opus_decoder_destroy, opus_decoder_get_nb_samples, opus_decoder_get_size, opus_decoder_init, }; -use crate::constants::max_frame_samples_for; +use crate::constants::{is_frame_size_2_5ms_aligned, max_frame_samples_for}; use crate::error::{Error, Result}; use crate::packet; use crate::types::{Bandwidth, Channels, SampleRate}; @@ -155,6 +155,12 @@ impl Decoder { if frame_size.get() > max_frame { return Err(Error::BadArg); } + // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms. + if (input.is_empty() || fec) + && !is_frame_size_2_5ms_aligned(frame_size.get(), self.sample_rate) + { + return Err(Error::BadArg); + } let input_len_i32 = if input.is_empty() { 0 @@ -210,6 +216,12 @@ impl Decoder { if frame_size.get() > max_frame { return Err(Error::BadArg); } + // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms. + if (input.is_empty() || fec) + && !is_frame_size_2_5ms_aligned(frame_size.get(), self.sample_rate) + { + return Err(Error::BadArg); + } let input_len_i32 = if input.is_empty() { 0 @@ -247,6 +259,9 @@ impl Decoder { /// overlong input, or a mapped libopus error. pub fn packet_samples(&self, packet: &[u8]) -> Result { // Errors: InvalidState or libopus error mapped. + if packet.is_empty() { + return Err(Error::BadArg); + } if packet.len() > i32::MAX as usize { return Err(Error::BadArg); } @@ -459,9 +474,13 @@ impl<'a> DecoderRef<'a> { /// # Safety /// - `ptr` must point to valid, initialized memory of at least [`Decoder::size()`] bytes /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - `sample_rate` and `channels` must exactly match the decoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped /// + /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer + /// sizes against the wrong channel/rate and then call libopus with out-of-bounds buffers. + /// /// Use [`Decoder::init_in_place`] to initialize the memory before calling this. #[must_use] pub unsafe fn from_raw( diff --git a/src/dred.rs b/src/dred.rs index 7a9f188..34ba00f 100644 --- a/src/dred.rs +++ b/src/dred.rs @@ -7,7 +7,7 @@ use crate::bindings::{ opus_dred_decoder_get_size, opus_dred_decoder_init, opus_dred_free, opus_dred_get_size, opus_dred_parse, opus_dred_process, }; -use crate::constants::max_frame_samples_for; +use crate::constants::{is_frame_size_2_5ms_aligned, max_frame_samples_for}; use crate::decoder::Decoder; use crate::error::{Error, Result}; use crate::types::SampleRate; @@ -16,6 +16,9 @@ use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; +// libopus computes `100 * max_dred_samples / sampling_rate` in signed 32-bit math. +const MAX_SAFE_DRED_SAMPLES: usize = (i32::MAX as usize) / 100; + /// Managed handle for libopus `OpusDREDDecoder`. pub struct DredDecoder { raw: RawHandle, @@ -88,10 +91,13 @@ impl DredDecoder { /// /// # Errors /// - /// Returns [`Error::InternalError`] if libopus reports an invalid (negative) - /// size, indicating a mismatch with the bundled headers. + /// Returns [`Error::InternalError`] if libopus reports a non-positive size, + /// indicating an unexpected ABI/runtime mismatch. pub fn size() -> Result { let raw = unsafe { opus_dred_decoder_get_size() }; + if raw <= 0 { + return Err(Error::InternalError); + } usize::try_from(raw).map_err(|_| Error::InternalError) } @@ -133,7 +139,7 @@ impl DredDecoder { defer_processing: bool, ) -> Result { let len = i32::try_from(data.len()).map_err(|_| Error::BadArg)?; - let max_samples = i32::try_from(max_dred_samples).map_err(|_| Error::BadArg)?; + let max_samples = checked_max_dred_samples(max_dred_samples)?; let result = unsafe { opus_dred_parse( self.raw.as_ptr(), @@ -229,6 +235,13 @@ impl DredDecoder { } } +fn checked_max_dred_samples(max_dred_samples: usize) -> Result { + if max_dred_samples > MAX_SAFE_DRED_SAMPLES { + return Err(Error::BadArg); + } + i32::try_from(max_dred_samples).map_err(|_| Error::BadArg) +} + impl<'a> DredDecoderRef<'a> { /// Wrap an externally-initialized DRED decoder without taking ownership. /// @@ -297,6 +310,10 @@ fn validate_pcm_frame_len( if frame_size_per_ch == 0 || frame_size_per_ch > max_frame_samples_for(sample_rate) { return Err(Error::BadArg); } + // libopus requires DRED decode frame sizes to be multiples of 2.5 ms. + if !is_frame_size_2_5ms_aligned(frame_size_per_ch, sample_rate) { + return Err(Error::BadArg); + } i32::try_from(frame_size_per_ch).map_err(|_| Error::BadArg) } @@ -316,12 +333,15 @@ impl DredState { /// Returns [`Error::AllocFail`] if allocation fails or a mapped libopus error when /// creation does not succeed. pub fn new() -> Result { + let size = Self::size()?; let mut err = 0; let ptr = unsafe { opus_dred_alloc(std::ptr::addr_of_mut!(err)) }; if err != 0 { return Err(Error::from_code(err)); } let ptr = NonNull::new(ptr).ok_or(Error::AllocFail)?; + // opus_dred_alloc() is malloc-like and does not initialize OpusDRED. + unsafe { std::ptr::write_bytes(ptr.as_ptr().cast::(), 0, size) }; Ok(Self { raw: ptr }) } @@ -360,7 +380,8 @@ mod tests { #[test] fn validate_pcm_frame_len_checks_arguments() { - let pcm = vec![0i16; 4]; + // 2.5 ms at 48 kHz = 120 samples/ch, so 240 total for stereo. + let pcm = vec![0i16; 240]; assert!(validate_pcm_frame_len(&pcm, 2, SampleRate::Hz48000).is_ok()); let err = validate_pcm_frame_len(&pcm, 0, SampleRate::Hz48000).unwrap_err(); @@ -371,5 +392,39 @@ mod tests { let err = validate_pcm_frame_len(&[] as &[i16], 2, SampleRate::Hz48000).unwrap_err(); assert_eq!(err, Error::BadArg); + + // Non-2.5ms-aligned frame size must be rejected. + let bad_pcm = vec![0i16; 4]; + let err = validate_pcm_frame_len(&bad_pcm, 2, SampleRate::Hz48000).unwrap_err(); + assert_eq!(err, Error::BadArg); + } + + #[test] + fn checked_max_dred_samples_blocks_overflow_inputs() { + assert_eq!( + checked_max_dred_samples(MAX_SAFE_DRED_SAMPLES), + Ok(i32::MAX / 100) + ); + assert_eq!( + checked_max_dred_samples(MAX_SAFE_DRED_SAMPLES + 1), + Err(Error::BadArg) + ); + } + + #[test] + fn fresh_dred_state_is_inactive() { + let mut decoder = match DredDecoder::new() { + Ok(decoder) => decoder, + Err(Error::Unimplemented) => return, + Err(err) => panic!("unexpected DRED decoder error: {err:?}"), + }; + let state = match DredState::new() { + Ok(state) => state, + Err(Error::Unimplemented) => return, + Err(err) => panic!("unexpected DRED state error: {err:?}"), + }; + let mut dst = DredState::new().unwrap(); + + assert_eq!(decoder.process(&state, &mut dst), Err(Error::BadArg)); } } diff --git a/src/encoder.rs b/src/encoder.rs index 0b65bd9..fc369bf 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -744,9 +744,13 @@ impl<'a> EncoderRef<'a> { /// # Safety /// - `ptr` must point to valid, initialized memory of at least [`Encoder::size()`] bytes /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - `sample_rate` and `channels` must exactly match the encoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped /// + /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer + /// sizes against the wrong channel/rate and then call libopus with out-of-bounds buffers. + /// /// Use [`Encoder::init_in_place`] to initialize the memory before calling this. #[must_use] pub unsafe fn from_raw( diff --git a/src/multistream.rs b/src/multistream.rs index 21e00db..c8aa292 100644 --- a/src/multistream.rs +++ b/src/multistream.rs @@ -26,7 +26,7 @@ use crate::bindings::{ opus_multistream_encoder_init, opus_multistream_surround_encoder_create, opus_multistream_surround_encoder_get_size, opus_multistream_surround_encoder_init, }; -use crate::constants::max_frame_samples_for; +use crate::constants::{is_frame_size_2_5ms_aligned, max_frame_samples_for}; use crate::error::{Error, Result}; use crate::types::{Application, Bandwidth, Bitrate, Channels, Complexity, SampleRate, Signal}; use crate::{AlignedBuffer, Ownership, RawHandle}; @@ -800,9 +800,13 @@ impl<'a> MultistreamEncoderRef<'a> { /// # Safety /// - `ptr` must point to valid, initialized memory of at least [`MultistreamEncoder::size()`] bytes /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - `sr` and `mapping` must exactly match the encoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped /// + /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer + /// sizes with the wrong layout and then call libopus with out-of-bounds buffers. + /// /// Use [`MultistreamEncoder::init_in_place`] to initialize the memory before calling this. #[must_use] pub unsafe fn from_raw(ptr: *mut OpusMSEncoder, sr: SampleRate, mapping: Mapping<'_>) -> Self { @@ -1015,6 +1019,12 @@ impl MultistreamDecoder { if out.len() != frame_size_per_ch.get() * self.channels as usize { return Err(Error::BadArg); } + // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms. + if (packet.is_empty() || fec) + && !is_frame_size_2_5ms_aligned(frame_size_per_ch.get(), self.sample_rate) + { + return Err(Error::BadArg); + } let n = unsafe { opus_multistream_decode( self.raw.as_ptr(), @@ -1058,6 +1068,12 @@ impl MultistreamDecoder { if out.len() != frame_size_per_ch.get() * self.channels as usize { return Err(Error::BadArg); } + // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms. + if (packet.is_empty() || fec) + && !is_frame_size_2_5ms_aligned(frame_size_per_ch.get(), self.sample_rate) + { + return Err(Error::BadArg); + } let n = unsafe { opus_multistream_decode_float( self.raw.as_ptr(), @@ -1307,9 +1323,13 @@ impl<'a> MultistreamDecoderRef<'a> { /// # Safety /// - `ptr` must point to valid, initialized memory of at least [`MultistreamDecoder::size()`] bytes /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - `sr` and `mapping` must exactly match the decoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped /// + /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer + /// sizes with the wrong layout and then call libopus with out-of-bounds buffers. + /// /// Use [`MultistreamDecoder::init_in_place`] to initialize the memory before calling this. #[must_use] pub unsafe fn from_raw(ptr: *mut OpusMSDecoder, sr: SampleRate, mapping: Mapping<'_>) -> Self { diff --git a/src/packet.rs b/src/packet.rs index d1f4246..d389efd 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -2,18 +2,30 @@ #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_possible_wrap)] +#![cfg_attr(not(opus_codec_rust_packet_ops), allow(dead_code))] use crate::bindings::{ OPUS_BANDWIDTH_FULLBAND, OPUS_BANDWIDTH_MEDIUMBAND, OPUS_BANDWIDTH_NARROWBAND, - OPUS_BANDWIDTH_SUPERWIDEBAND, OPUS_BANDWIDTH_WIDEBAND, opus_multistream_packet_pad, - opus_multistream_packet_unpad, opus_packet_get_bandwidth, opus_packet_get_nb_channels, - opus_packet_get_nb_frames, opus_packet_get_nb_samples, opus_packet_get_samples_per_frame, - opus_packet_has_lbrr, opus_packet_pad, opus_packet_parse, opus_packet_unpad, - opus_pcm_soft_clip, + OPUS_BANDWIDTH_SUPERWIDEBAND, OPUS_BANDWIDTH_WIDEBAND, opus_multistream_packet_unpad, + opus_packet_get_bandwidth, opus_packet_get_nb_channels, opus_packet_get_nb_frames, + opus_packet_get_nb_samples, opus_packet_get_samples_per_frame, opus_packet_has_lbrr, + opus_packet_parse, opus_packet_unpad, opus_pcm_soft_clip, }; +#[cfg(not(opus_codec_rust_packet_ops))] +use crate::bindings::{opus_multistream_packet_pad, opus_packet_pad}; use crate::error::{Error, Result}; use crate::types::{Bandwidth, Channels, SampleRate}; +mod layout; + +use layout::MAX_FRAMES_PER_PACKET; +#[cfg(opus_codec_rust_packet_ops)] +pub(crate) use layout::{ + RepacketizerInputs, packet_repacketizer_inputs, repacketize_frames, repacketize_frames_range, +}; +#[cfg(opus_codec_rust_packet_ops)] +use layout::{multistream_last_stream_offset, pad_single_packet}; + /// Get bandwidth from a packet. /// /// # Errors @@ -165,9 +177,10 @@ pub fn packet_parse(packet: &[u8]) -> Result<(u8, usize, Vec<&[u8]>)> { } let mut out_toc: u8 = 0; let mut payload_offset: i32 = 0; - // libopus caps frames at 48 according to docs - let mut frames_ptrs: [*const u8; 48] = [std::ptr::null(); 48]; - let mut sizes: [i16; 48] = [0; 48]; + // libopus caps frames at MAX_FRAMES_PER_PACKET according to docs. + let mut frames_ptrs: [*const u8; MAX_FRAMES_PER_PACKET] = + [std::ptr::null(); MAX_FRAMES_PER_PACKET]; + let mut sizes: [i16; MAX_FRAMES_PER_PACKET] = [0; MAX_FRAMES_PER_PACKET]; let len_i32 = i32::try_from(packet.len()).map_err(|_| Error::BadArg)?; let n = unsafe { opus_packet_parse( @@ -197,7 +210,7 @@ pub fn packet_parse(packet: &[u8]) -> Result<(u8, usize, Vec<&[u8]>)> { } // SAFETY: pointers are into `packet`; derive offset via pointer arithmetic let start = ptr_addr - base_addr; - let end = start + size; + let end = start.checked_add(size).ok_or(Error::InternalError)?; if end > packet.len() { return Err(Error::InvalidPacket); } @@ -210,14 +223,36 @@ pub fn packet_parse(packet: &[u8]) -> Result<(u8, usize, Vec<&[u8]>)> { )) } +/// Increase a packet's size by adding padding to reach `new_len`. +/// +/// # Errors +/// Returns [`Error::BadArg`] for invalid lengths or another error if padding fails. +#[cfg(opus_codec_rust_packet_ops)] +pub fn packet_pad(packet: &mut [u8], len: usize, new_len: usize) -> Result<()> { + if new_len < len || new_len > packet.len() { + return Err(Error::BadArg); + } + if len == 0 { + return Err(Error::BadArg); + } + if len == new_len { + return Ok(()); + } + pad_single_packet(packet, len, new_len) +} + /// Increase a packet's size by adding padding to reach `new_len`. /// /// # Errors /// Returns [`Error::BadArg`] for invalid lengths or a mapped libopus error if padding fails. +#[cfg(not(opus_codec_rust_packet_ops))] pub fn packet_pad(packet: &mut [u8], len: usize, new_len: usize) -> Result<()> { if new_len < len || new_len > packet.len() { return Err(Error::BadArg); } + if len == 0 { + return Err(Error::BadArg); + } let len_i32 = i32::try_from(len).map_err(|_| Error::BadArg)?; let new_len_i32 = i32::try_from(new_len).map_err(|_| Error::BadArg)?; let r = unsafe { opus_packet_pad(packet.as_mut_ptr(), len_i32, new_len_i32) }; @@ -235,6 +270,9 @@ pub fn packet_unpad(packet: &mut [u8], len: usize) -> Result { if len > packet.len() { return Err(Error::BadArg); } + if len == 0 { + return Err(Error::BadArg); + } let len_i32 = i32::try_from(len).map_err(|_| Error::BadArg)?; let n = unsafe { opus_packet_unpad(packet.as_mut_ptr(), len_i32) }; if n < 0 { @@ -243,10 +281,44 @@ pub fn packet_unpad(packet: &mut [u8], len: usize) -> Result { usize::try_from(n).map_err(|_| Error::InternalError) } +/// Pad a multistream packet to `new_len` given `nb_streams`. +/// +/// # Errors +/// Returns [`Error::BadArg`] for invalid lengths or another error if padding fails. +#[cfg(opus_codec_rust_packet_ops)] +pub fn multistream_packet_pad( + packet: &mut [u8], + len: usize, + new_len: usize, + nb_streams: i32, +) -> Result<()> { + if new_len < len || new_len > packet.len() { + return Err(Error::BadArg); + } + if len == 0 { + return Err(Error::BadArg); + } + // The public API requires at least one stream. Reject invalid counts + // before delegating to libopus' multistream packet walker. + if nb_streams < 1 { + return Err(Error::BadArg); + } + if len == new_len { + return Ok(()); + } + let nb_streams = usize::try_from(nb_streams).map_err(|_| Error::BadArg)?; + let last_stream_offset = multistream_last_stream_offset(&packet[..len], nb_streams)?; + let amount = new_len - len; + let last_len = len - last_stream_offset; + let last_new_len = last_len.checked_add(amount).ok_or(Error::BadArg)?; + pad_single_packet(&mut packet[last_stream_offset..], last_len, last_new_len) +} + /// Pad a multistream packet to `new_len` given `nb_streams`. /// /// # Errors /// Returns [`Error::BadArg`] for invalid lengths or a mapped libopus error if padding fails. +#[cfg(not(opus_codec_rust_packet_ops))] pub fn multistream_packet_pad( packet: &mut [u8], len: usize, @@ -256,6 +328,12 @@ pub fn multistream_packet_pad( if new_len < len || new_len > packet.len() { return Err(Error::BadArg); } + if len == 0 { + return Err(Error::BadArg); + } + if nb_streams < 1 { + return Err(Error::BadArg); + } let len_i32 = i32::try_from(len).map_err(|_| Error::BadArg)?; let new_len_i32 = i32::try_from(new_len).map_err(|_| Error::BadArg)?; let r = unsafe { @@ -275,6 +353,14 @@ pub fn multistream_packet_unpad(packet: &mut [u8], len: usize, nb_streams: i32) if len > packet.len() { return Err(Error::BadArg); } + if len == 0 { + return Err(Error::BadArg); + } + // The public API requires at least one stream. Reject invalid counts + // before delegating to libopus' multistream packet walker. + if nb_streams < 1 { + return Err(Error::BadArg); + } let len_i32 = i32::try_from(len).map_err(|_| Error::BadArg)?; let n = unsafe { opus_multistream_packet_unpad(packet.as_mut_ptr(), len_i32, nb_streams) }; if n < 0 { diff --git a/src/packet/layout.rs b/src/packet/layout.rs new file mode 100644 index 0000000..70fcaf6 --- /dev/null +++ b/src/packet/layout.rs @@ -0,0 +1,748 @@ +#![cfg_attr(not(opus_codec_rust_packet_ops), allow(dead_code))] + +use crate::bindings::opus_packet_get_samples_per_frame; +use crate::error::{Error, Result}; + +mod extensions; + +use extensions::{generate_extensions, parse_existing_extensions}; + +#[derive(Clone, Copy, Debug)] +struct FrameSpan { + start: usize, + len: usize, +} + +impl FrameSpan { + fn slice(self, packet: &[u8]) -> Result<&[u8]> { + let end = self + .start + .checked_add(self.len) + .ok_or(Error::InternalError)?; + packet.get(self.start..end).ok_or(Error::InvalidPacket) + } +} + +#[derive(Debug)] +struct ParsedPacket { + toc: u8, + frames: Vec, + padding_start: usize, + packet_offset: usize, +} + +#[derive(Debug)] +struct PacketExtension { + id: u8, + frame: u8, + data: Vec, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct PacketPadding<'a> { + data: &'a [u8], + frame_count: usize, +} + +pub(crate) type RepacketizerInputs<'a> = (u8, Vec<&'a [u8]>, Vec>); + +// The packet writer/parser below mirrors the libopus packet layout helpers: +// opus.c::opus_packet_parse_impl(), repacketizer.c::opus_repacketizer_out_range_impl(), +// and extensions.c::opus_packet_extensions_*(). +const OPUS_REFERENCE_SAMPLE_RATE: i32 = 48_000; +pub(super) const MAX_FRAMES_PER_PACKET: usize = 48; +const MAX_FRAME_PAYLOAD_BYTES: usize = 1275; +const MAX_PACKET_SAMPLES_48KHZ: usize = 5760; + +const TOC_CODE_MASK: u8 = 0x03; +const TOC_CONFIG_MASK: u8 = 0xFC; +const PACKET_CODE_1: u8 = 0x01; +const PACKET_CODE_2: u8 = 0x02; +const PACKET_CODE_3: u8 = 0x03; + +const CODE_3_FRAME_COUNT_MASK: u8 = 0x3F; +const CODE_3_PADDING_FLAG: u8 = 0x40; +const CODE_3_VBR_FLAG: u8 = 0x80; + +const SIZE_TWO_BYTE_THRESHOLD: usize = 252; +const SIZE_LOW_BITS_MASK: usize = 0x03; + +const PADDING_CONTINUATION_BYTE: u8 = 255; +const PADDING_CONTINUATION_AMOUNT: usize = 254; + +const EXTENSION_PADDING_ID: u8 = 0; +const EXTENSION_FRAME_SEPARATOR_ID: u8 = 1; +#[cfg(not(opus_codec_frame_bounded_extensions))] +const EXTENSION_MIN_DATA_ID: u8 = 2; +#[cfg(opus_codec_frame_bounded_extensions)] +const EXTENSION_REPEAT_ID: u8 = 2; +#[cfg(opus_codec_frame_bounded_extensions)] +const EXTENSION_FRAME_BOUNDED_MIN_DATA_ID: u8 = 3; +const EXTENSION_SHORT_ID_MAX: u8 = 32; +const EXTENSION_MAX_ID: u8 = 127; +const EXTENSION_PADDING_BYTE: u8 = 0x01; +const EXTENSION_ONE_FRAME_SEPARATOR: u8 = 0x02; +const EXTENSION_MULTI_FRAME_SEPARATOR: u8 = 0x03; +const EXTENSION_LENGTH_CHUNK: usize = 255; +const EXTENSION_LENGTH_CHUNK_BYTE: u8 = 255; + +#[derive(Debug, Default)] +struct PacketWritePlan { + total_size: usize, + cursor: usize, + extension_padding_begin: usize, + extension_padding_end: usize, + extensions_begin: usize, + extension_bytes: Vec, +} + +#[derive(Debug)] +struct PacketLayoutParser<'a> { + data: &'a [u8], + self_delimited: bool, + sizes: [usize; MAX_FRAMES_PER_PACKET], + cursor: usize, + remaining: usize, + count: usize, + cbr: bool, + last_size: usize, + padding_len: usize, +} + +fn encoded_size_len(size: usize) -> usize { + if size < SIZE_TWO_BYTE_THRESHOLD { 1 } else { 2 } +} + +fn encode_size(size: usize, out: &mut [u8]) -> Result { + if out.is_empty() { + return Err(Error::BufferTooSmall); + } + if size < SIZE_TWO_BYTE_THRESHOLD { + out[0] = u8::try_from(size).map_err(|_| Error::InternalError)?; + return Ok(1); + } + if out.len() < 2 { + return Err(Error::BufferTooSmall); + } + let low = SIZE_TWO_BYTE_THRESHOLD + (size & SIZE_LOW_BITS_MASK); + out[0] = u8::try_from(low).map_err(|_| Error::InternalError)?; + out[1] = u8::try_from((size - low) >> 2).map_err(|_| Error::InternalError)?; + Ok(2) +} + +fn parse_size(data: &[u8]) -> Option<(usize, usize)> { + if data.is_empty() { + None + } else if usize::from(data[0]) < SIZE_TWO_BYTE_THRESHOLD { + Some((1, usize::from(data[0]))) + } else if data.len() < 2 { + None + } else { + Some((2, 4 * usize::from(data[1]) + usize::from(data[0]))) + } +} + +impl<'a> PacketLayoutParser<'a> { + fn new(data: &'a [u8], self_delimited: bool) -> Result { + if data.is_empty() { + return Err(Error::InvalidPacket); + } + Ok(Self { + data, + self_delimited, + sizes: [0usize; MAX_FRAMES_PER_PACKET], + cursor: 1, + remaining: data.len() - 1, + count: 0, + cbr: false, + last_size: data.len() - 1, + padding_len: 0, + }) + } + + fn parse(mut self) -> Result { + let toc = self.data[0]; + let frame_samples = usize::try_from(unsafe { + opus_packet_get_samples_per_frame(self.data.as_ptr(), OPUS_REFERENCE_SAMPLE_RATE) + }) + .map_err(|_| Error::InvalidPacket)?; + self.parse_header(toc, frame_samples)?; + self.finish_last_frame_size()?; + let (frames, padding_start) = self.collect_frames()?; + let packet_offset = padding_start + .checked_add(self.padding_len) + .ok_or(Error::InternalError)?; + if packet_offset > self.data.len() { + return Err(Error::InvalidPacket); + } + + Ok(ParsedPacket { + toc, + frames, + padding_start, + packet_offset, + }) + } + + fn parse_header(&mut self, toc: u8, frame_samples: usize) -> Result<()> { + match toc & TOC_CODE_MASK { + 0 => self.parse_single_frame(), + PACKET_CODE_1 => self.parse_two_frame_cbr()?, + PACKET_CODE_2 => self.parse_two_frame_vbr()?, + _ => self.parse_many_frame_packet(frame_samples)?, + } + Ok(()) + } + + fn parse_single_frame(&mut self) { + self.count = 1; + } + + fn parse_two_frame_cbr(&mut self) -> Result<()> { + self.count = 2; + self.cbr = true; + if !self.self_delimited { + if !self.remaining.is_multiple_of(2) { + return Err(Error::InvalidPacket); + } + self.last_size = self.remaining / 2; + self.sizes[0] = self.last_size; + } + Ok(()) + } + + fn parse_two_frame_vbr(&mut self) -> Result<()> { + self.count = 2; + let (_, size) = self.read_size_field()?; + self.sizes[0] = size; + self.last_size = self.remaining - size; + Ok(()) + } + + fn parse_many_frame_packet(&mut self, frame_samples: usize) -> Result<()> { + let ch = self.read_byte()?; + self.count = usize::from(ch & CODE_3_FRAME_COUNT_MASK); + self.validate_frame_count(frame_samples)?; + if ch & CODE_3_PADDING_FLAG != 0 { + self.parse_padding_length()?; + } + + self.cbr = ch & CODE_3_VBR_FLAG == 0; + if self.cbr { + if !self.self_delimited { + self.parse_many_frame_cbr()?; + } + } else { + self.parse_many_frame_vbr()?; + } + Ok(()) + } + + fn validate_frame_count(&self, frame_samples: usize) -> Result<()> { + let total_duration = frame_samples + .checked_mul(self.count) + .ok_or(Error::InvalidPacket)?; + if self.count == 0 + || self.count > MAX_FRAMES_PER_PACKET + || total_duration > MAX_PACKET_SAMPLES_48KHZ + { + return Err(Error::InvalidPacket); + } + Ok(()) + } + + fn parse_padding_length(&mut self) -> Result<()> { + loop { + let byte = self.read_byte()?; + let chunk = if byte == PADDING_CONTINUATION_BYTE { + PADDING_CONTINUATION_AMOUNT + } else { + usize::from(byte) + }; + if chunk > self.remaining { + return Err(Error::InvalidPacket); + } + self.remaining -= chunk; + self.padding_len = self + .padding_len + .checked_add(chunk) + .ok_or(Error::InternalError)?; + if byte != PADDING_CONTINUATION_BYTE { + return Ok(()); + } + } + } + + fn parse_many_frame_vbr(&mut self) -> Result<()> { + self.last_size = self.remaining; + for index in 0..(self.count - 1) { + let (bytes, size) = self.read_size_field()?; + self.sizes[index] = size; + let consumed = bytes.checked_add(size).ok_or(Error::InternalError)?; + if consumed > self.last_size { + return Err(Error::InvalidPacket); + } + self.last_size -= consumed; + } + Ok(()) + } + + fn parse_many_frame_cbr(&mut self) -> Result<()> { + self.last_size = self.remaining / self.count; + if self + .last_size + .checked_mul(self.count) + .ok_or(Error::InternalError)? + != self.remaining + { + return Err(Error::InvalidPacket); + } + for size in self.sizes.iter_mut().take(self.count - 1) { + *size = self.last_size; + } + Ok(()) + } + + fn finish_last_frame_size(&mut self) -> Result<()> { + if self.self_delimited { + let (bytes, last) = self.read_size_field()?; + if self.cbr { + if last.checked_mul(self.count).ok_or(Error::InternalError)? > self.remaining { + return Err(Error::InvalidPacket); + } + for size in self.sizes.iter_mut().take(self.count - 1) { + *size = last; + } + } else if bytes.checked_add(last).ok_or(Error::InternalError)? > self.last_size { + return Err(Error::InvalidPacket); + } + self.sizes[self.count - 1] = last; + return Ok(()); + } + + if self.last_size > MAX_FRAME_PAYLOAD_BYTES { + return Err(Error::InvalidPacket); + } + self.sizes[self.count - 1] = self.last_size; + Ok(()) + } + + fn read_byte(&mut self) -> Result { + let byte = *self.data.get(self.cursor).ok_or(Error::InvalidPacket)?; + self.consume_header_bytes(1)?; + Ok(byte) + } + + fn read_size_field(&mut self) -> Result<(usize, usize)> { + let (bytes, size) = parse_size(&self.data[self.cursor..]).ok_or(Error::InvalidPacket)?; + self.consume_header_bytes(bytes)?; + if size > self.remaining { + return Err(Error::InvalidPacket); + } + Ok((bytes, size)) + } + + fn consume_header_bytes(&mut self, bytes: usize) -> Result<()> { + if bytes > self.remaining { + return Err(Error::InvalidPacket); + } + self.cursor = self.cursor.checked_add(bytes).ok_or(Error::InternalError)?; + self.remaining -= bytes; + Ok(()) + } + + fn collect_frames(&self) -> Result<(Vec, usize)> { + let mut frame_cursor = self.cursor; + let mut frames = Vec::with_capacity(self.count); + for &len in self.sizes.iter().take(self.count) { + let end = frame_cursor.checked_add(len).ok_or(Error::InternalError)?; + if end > self.data.len() { + return Err(Error::InvalidPacket); + } + frames.push(FrameSpan { + start: frame_cursor, + len, + }); + frame_cursor = end; + } + Ok((frames, frame_cursor)) + } +} + +fn parse_packet_layout(data: &[u8], self_delimited: bool) -> Result { + PacketLayoutParser::new(data, self_delimited)?.parse() +} + +fn ensure_output_capacity(required: usize, out: &[u8]) -> Result<()> { + if required > out.len() { + return Err(Error::BufferTooSmall); + } + Ok(()) +} + +fn write_compact_packet_header( + toc: u8, + frame_lengths: &[usize], + out: &mut [u8], +) -> Result { + match frame_lengths { + [] => Err(Error::InvalidPacket), + [first] => { + let total_size = first.checked_add(1).ok_or(Error::InternalError)?; + ensure_output_capacity(total_size, out)?; + out[0] = toc & TOC_CONFIG_MASK; + Ok(PacketWritePlan { + total_size, + cursor: 1, + ..PacketWritePlan::default() + }) + } + [first, second] if second == first => { + let total_size = first + .checked_mul(2) + .and_then(|size| size.checked_add(1)) + .ok_or(Error::InternalError)?; + ensure_output_capacity(total_size, out)?; + out[0] = (toc & TOC_CONFIG_MASK) | PACKET_CODE_1; + Ok(PacketWritePlan { + total_size, + cursor: 1, + ..PacketWritePlan::default() + }) + } + [first, second] => { + let total_size = first + .checked_add(*second) + .and_then(|size| size.checked_add(1 + encoded_size_len(*first))) + .ok_or(Error::InternalError)?; + ensure_output_capacity(total_size, out)?; + out[0] = (toc & TOC_CONFIG_MASK) | PACKET_CODE_2; + let mut plan = PacketWritePlan { + total_size, + cursor: 1, + ..PacketWritePlan::default() + }; + plan.cursor += encode_size(*first, &mut out[plan.cursor..])?; + Ok(plan) + } + _ => Ok(PacketWritePlan::default()), + } +} + +fn frame_lengths_are_vbr(frame_lengths: &[usize]) -> bool { + frame_lengths[1..] + .iter() + .any(|&len| len != frame_lengths[0]) +} + +fn write_general_packet_header( + toc: u8, + frame_lengths: &[usize], + extensions: &[PacketExtension], + out: &mut [u8], +) -> Result { + let count = frame_lengths.len(); + let vbr = frame_lengths_are_vbr(frame_lengths); + let mut plan = PacketWritePlan::default(); + + if vbr { + plan.total_size = 2; + for &len in frame_lengths.iter().take(count - 1) { + plan.total_size = plan + .total_size + .checked_add(encoded_size_len(len)) + .and_then(|size| size.checked_add(len)) + .ok_or(Error::InternalError)?; + } + plan.total_size = plan + .total_size + .checked_add(frame_lengths[count - 1]) + .ok_or(Error::InternalError)?; + ensure_output_capacity(plan.total_size, out)?; + out[0] = (toc & TOC_CONFIG_MASK) | PACKET_CODE_3; + out[1] = u8::try_from(count).map_err(|_| Error::InternalError)? | CODE_3_VBR_FLAG; + } else { + plan.total_size = frame_lengths[0] + .checked_mul(count) + .and_then(|size| size.checked_add(2)) + .ok_or(Error::InternalError)?; + ensure_output_capacity(plan.total_size, out)?; + out[0] = (toc & TOC_CONFIG_MASK) | PACKET_CODE_3; + out[1] = u8::try_from(count).map_err(|_| Error::InternalError)?; + } + plan.cursor = 2; + plan.extension_bytes = generate_extensions(extensions, out.len() - plan.total_size, count)?; + + let pad_amount = out.len() - plan.total_size; + if pad_amount != 0 { + let nb_255s = (pad_amount - 1) / usize::from(PADDING_CONTINUATION_BYTE); + let overhead = plan + .total_size + .checked_add(plan.extension_bytes.len()) + .and_then(|size| size.checked_add(nb_255s + 1)) + .ok_or(Error::InternalError)?; + ensure_output_capacity(overhead, out)?; + out[1] |= CODE_3_PADDING_FLAG; + plan.extensions_begin = plan.total_size + pad_amount - plan.extension_bytes.len(); + plan.extension_padding_begin = plan.total_size + nb_255s + 1; + plan.extension_padding_end = out.len() - plan.extension_bytes.len(); + out[plan.cursor..plan.cursor + nb_255s].fill(PADDING_CONTINUATION_BYTE); + plan.cursor += nb_255s; + out[plan.cursor] = + u8::try_from(pad_amount - usize::from(PADDING_CONTINUATION_BYTE) * nb_255s - 1) + .map_err(|_| Error::InternalError)?; + plan.cursor += 1; + plan.total_size += pad_amount; + } + + if vbr { + for &len in frame_lengths.iter().take(count - 1) { + plan.cursor += encode_size(len, &mut out[plan.cursor..])?; + } + } + Ok(plan) +} + +fn compact_packet_len(frame_lengths: &[usize]) -> Result> { + match frame_lengths { + [] => Err(Error::InvalidPacket), + [first] => first.checked_add(1).map(Some).ok_or(Error::InternalError), + [first, second] if second == first => first + .checked_mul(2) + .and_then(|size| size.checked_add(1)) + .map(Some) + .ok_or(Error::InternalError), + [first, second] => first + .checked_add(*second) + .and_then(|size| size.checked_add(1 + encoded_size_len(*first))) + .map(Some) + .ok_or(Error::InternalError), + _ => Ok(None), + } +} + +fn general_packet_base_len(frame_lengths: &[usize]) -> Result { + let count = frame_lengths.len(); + if count == 0 { + return Err(Error::InvalidPacket); + } + + if frame_lengths_are_vbr(frame_lengths) { + let mut total_size = 2usize; + for &len in frame_lengths.iter().take(count - 1) { + total_size = total_size + .checked_add(encoded_size_len(len)) + .and_then(|size| size.checked_add(len)) + .ok_or(Error::InternalError)?; + } + total_size + .checked_add(frame_lengths[count - 1]) + .ok_or(Error::InternalError) + } else { + frame_lengths[0] + .checked_mul(count) + .and_then(|size| size.checked_add(2)) + .ok_or(Error::InternalError) + } +} + +fn copy_frame_slices(frames: &[&[u8]], cursor: &mut usize, out: &mut [u8]) -> Result<()> { + for frame in frames { + let dst_end = cursor + .checked_add(frame.len()) + .ok_or(Error::InternalError)?; + if dst_end > out.len() { + return Err(Error::BufferTooSmall); + } + out[*cursor..dst_end].copy_from_slice(frame); + *cursor = dst_end; + } + Ok(()) +} + +fn finish_packet_write(plan: &PacketWritePlan, cursor: usize, out: &mut [u8]) -> Result<()> { + if !plan.extension_bytes.is_empty() { + let ext_end = plan + .extensions_begin + .checked_add(plan.extension_bytes.len()) + .ok_or(Error::InternalError)?; + if ext_end > out.len() { + return Err(Error::BufferTooSmall); + } + out[plan.extensions_begin..ext_end].copy_from_slice(&plan.extension_bytes); + } + if plan.extension_padding_begin < plan.extension_padding_end { + out[plan.extension_padding_begin..plan.extension_padding_end].fill(EXTENSION_PADDING_BYTE); + } + if plan.extension_bytes.is_empty() && cursor < out.len() { + out[cursor..].fill(0); + } + Ok(()) +} + +fn collect_repacketizer_extensions_in_range( + paddings: &[PacketPadding<'_>], + begin: usize, + end: usize, +) -> Result> { + let mut extensions = Vec::new(); + for (owner_frame, padding) in paddings.iter().enumerate().take(end) { + let frame_extensions = match parse_existing_extensions(padding.data, padding.frame_count) { + Ok(extensions) => extensions, + Err(err) if (begin..end).contains(&owner_frame) => return Err(err), + Err(_) => continue, + }; + for mut extension in frame_extensions { + let frame = usize::from(extension.frame) + .checked_add(owner_frame) + .ok_or(Error::InternalError)?; + if (begin..end).contains(&frame) { + extension.frame = u8::try_from(frame - begin).map_err(|_| Error::InvalidPacket)?; + extensions.push(extension); + } + } + } + Ok(extensions) +} + +pub(crate) fn packet_repacketizer_inputs(packet: &[u8]) -> Result> { + let parsed = parse_packet_layout(packet, false)?; + if parsed.packet_offset != packet.len() { + return Err(Error::InvalidPacket); + } + + let frames = parsed + .frames + .iter() + .map(|frame| frame.slice(packet)) + .collect::>>()?; + + let empty = &packet[0..0]; + let mut paddings = vec![ + PacketPadding { + data: empty, + frame_count: 0, + }; + frames.len() + ]; + if !paddings.is_empty() { + paddings[0] = PacketPadding { + data: &packet[parsed.padding_start..parsed.packet_offset], + frame_count: frames.len(), + }; + } + + Ok((parsed.toc, frames, paddings)) +} + +pub(crate) fn repacketize_frames( + toc: u8, + frames: &[&[u8]], + paddings: &[PacketPadding<'_>], + out: &mut [u8], +) -> Result { + repacketize_frames_range(toc, frames, paddings, 0, frames.len(), out) +} + +pub(crate) fn repacketize_frames_range( + toc: u8, + frames: &[&[u8]], + paddings: &[PacketPadding<'_>], + begin: usize, + end: usize, + out: &mut [u8], +) -> Result { + if frames.len() != paddings.len() { + return Err(Error::InternalError); + } + if begin > end || end > frames.len() { + return Err(Error::BadArg); + } + + let frames = &frames[begin..end]; + let frame_lengths: Vec = frames.iter().map(|frame| frame.len()).collect(); + let extensions = collect_repacketizer_extensions_in_range(paddings, begin, end)?; + + let compact_len = if extensions.is_empty() { + compact_packet_len(&frame_lengths)? + } else { + None + }; + let total_len = if let Some(total_len) = compact_len { + total_len + } else { + let base_len = general_packet_base_len(&frame_lengths)?; + let ext_len = generate_extensions(&extensions, usize::MAX, frames.len())?.len(); + let ext_padding_len = if ext_len == 0 { + 0 + } else { + ext_len + .checked_add(ext_len / PADDING_CONTINUATION_AMOUNT) + .and_then(|len| len.checked_add(1)) + .ok_or(Error::InternalError)? + }; + base_len + .checked_add(ext_padding_len) + .ok_or(Error::InternalError)? + }; + + ensure_output_capacity(total_len, out)?; + let out = &mut out[..total_len]; + let mut plan = if compact_len.is_some() { + write_compact_packet_header(toc, &frame_lengths, out)? + } else { + write_general_packet_header(toc, &frame_lengths, &extensions, out)? + }; + + copy_frame_slices(frames, &mut plan.cursor, out)?; + finish_packet_write(&plan, plan.cursor, out)?; + debug_assert_eq!(plan.total_size, total_len); + Ok(total_len) +} + +#[cfg(opus_codec_rust_packet_ops)] +fn write_padded_packet(src: &[u8], parsed: &ParsedPacket, out: &mut [u8]) -> Result<()> { + let frame_lengths: Vec = parsed.frames.iter().map(|frame| frame.len).collect(); + let frames = parsed + .frames + .iter() + .map(|frame| frame.slice(src)) + .collect::>>()?; + let padding = &src[parsed.padding_start..parsed.packet_offset]; + let extensions = parse_existing_extensions(padding, frame_lengths.len())?; + let mut plan = write_compact_packet_header(parsed.toc, &frame_lengths, out)?; + + if frame_lengths.len() > 2 || plan.total_size < out.len() || !extensions.is_empty() { + plan = write_general_packet_header(parsed.toc, &frame_lengths, &extensions, out)?; + } + + copy_frame_slices(&frames, &mut plan.cursor, out)?; + finish_packet_write(&plan, plan.cursor, out)?; + debug_assert_eq!(plan.total_size, out.len()); + Ok(()) +} + +#[cfg(opus_codec_rust_packet_ops)] +pub(super) fn pad_single_packet(packet: &mut [u8], len: usize, new_len: usize) -> Result<()> { + let src = packet[..len].to_vec(); + let parsed = parse_packet_layout(&src, false)?; + if parsed.packet_offset != src.len() { + return Err(Error::InvalidPacket); + } + write_padded_packet(&src, &parsed, &mut packet[..new_len]) +} + +#[cfg(opus_codec_rust_packet_ops)] +pub(super) fn multistream_last_stream_offset(packet: &[u8], nb_streams: usize) -> Result { + let mut offset = 0usize; + for _ in 0..nb_streams.saturating_sub(1) { + let parsed = parse_packet_layout(&packet[offset..], true)?; + offset = offset + .checked_add(parsed.packet_offset) + .ok_or(Error::InternalError)?; + if offset > packet.len() { + return Err(Error::InvalidPacket); + } + } + Ok(offset) +} diff --git a/src/packet/layout/extensions.rs b/src/packet/layout/extensions.rs new file mode 100644 index 0000000..ff75061 --- /dev/null +++ b/src/packet/layout/extensions.rs @@ -0,0 +1,683 @@ +#![cfg_attr(not(opus_codec_rust_packet_ops), allow(dead_code))] + +#[cfg(not(opus_codec_frame_bounded_extensions))] +use super::EXTENSION_MIN_DATA_ID; +#[cfg(opus_codec_frame_bounded_extensions)] +use super::{EXTENSION_FRAME_BOUNDED_MIN_DATA_ID, EXTENSION_REPEAT_ID}; +use super::{ + EXTENSION_FRAME_SEPARATOR_ID, EXTENSION_LENGTH_CHUNK, EXTENSION_LENGTH_CHUNK_BYTE, + EXTENSION_MAX_ID, EXTENSION_MULTI_FRAME_SEPARATOR, EXTENSION_ONE_FRAME_SEPARATOR, + EXTENSION_PADDING_ID, EXTENSION_SHORT_ID_MAX, MAX_FRAMES_PER_PACKET, PacketExtension, +}; +use crate::error::{Error, Result}; + +#[cfg(not(opus_codec_frame_bounded_extensions))] +fn skip_extension(data: &[u8], start: usize) -> Result<(usize, usize)> { + let remaining = data.len().saturating_sub(start); + if remaining == 0 { + return Ok((start, 0)); + } + + let first = data[start]; + let id = first >> 1; + let len_flag = first & 1; + if id == EXTENSION_PADDING_ID && len_flag == 1 { + return Ok((start + 1, 1)); + } + if id > EXTENSION_PADDING_ID && id < EXTENSION_SHORT_ID_MAX { + let consumed = 1 + usize::from(len_flag); + if remaining < consumed { + return Err(Error::InvalidPacket); + } + return Ok((start + consumed, 1)); + } + if len_flag == 0 { + return Ok((data.len(), 1)); + } + + let mut cursor = start + 1; + let mut header_size = 1usize; + let mut payload_len = 0usize; + loop { + if cursor >= data.len() { + return Err(Error::InvalidPacket); + } + let byte = usize::from(data[cursor]); + payload_len = payload_len.checked_add(byte).ok_or(Error::InternalError)?; + header_size += 1; + cursor += 1; + if byte != EXTENSION_LENGTH_CHUNK { + break; + } + } + + if payload_len > data.len().saturating_sub(cursor) { + return Err(Error::InvalidPacket); + } + Ok((cursor + payload_len, header_size)) +} + +#[cfg(opus_codec_frame_bounded_extensions)] +fn skip_extension_payload_frame_bounded( + data: &[u8], + start: usize, + id_byte: u8, + trailing_short_len: usize, +) -> Result<(usize, usize)> { + let id = id_byte >> 1; + let len_flag = id_byte & 1; + let mut cursor = start; + let mut header_size = 0usize; + + if (id == EXTENSION_PADDING_ID && len_flag == 1) || id == EXTENSION_REPEAT_ID { + return Ok((cursor, header_size)); + } + if id > EXTENSION_PADDING_ID && id < EXTENSION_SHORT_ID_MAX { + let consumed = usize::from(len_flag); + if data.len().saturating_sub(cursor) < consumed { + return Err(Error::InvalidPacket); + } + return Ok((cursor + consumed, header_size)); + } + if len_flag == 0 { + if data.len().saturating_sub(cursor) < trailing_short_len { + return Err(Error::InvalidPacket); + } + return Ok((data.len() - trailing_short_len, header_size)); + } + + let mut payload_len = 0usize; + loop { + if cursor >= data.len() { + return Err(Error::InvalidPacket); + } + let byte = usize::from(data[cursor]); + payload_len = payload_len.checked_add(byte).ok_or(Error::InternalError)?; + header_size += 1; + cursor += 1; + if byte != EXTENSION_LENGTH_CHUNK { + break; + } + } + + if payload_len > data.len().saturating_sub(cursor) { + return Err(Error::InvalidPacket); + } + Ok((cursor + payload_len, header_size)) +} + +#[cfg(opus_codec_frame_bounded_extensions)] +fn skip_extension_frame_bounded(data: &[u8], start: usize) -> Result<(usize, usize)> { + let id_byte = *data.get(start).ok_or(Error::InvalidPacket)?; + let (next, payload_header_size) = + skip_extension_payload_frame_bounded(data, start + 1, id_byte, 0)?; + Ok((next, payload_header_size + 1)) +} + +#[cfg(not(opus_codec_frame_bounded_extensions))] +fn parse_extensions(data: &[u8], _frame_count: usize) -> Result> { + let mut cursor = 0usize; + let mut current_frame = 0usize; + let mut extensions = Vec::new(); + + while cursor < data.len() { + let entry = data[cursor]; + let id = entry >> 1; + let len_flag = entry & 1; + let (next, header_size) = skip_extension(data, cursor)?; + if id >= EXTENSION_MIN_DATA_ID { + if current_frame >= MAX_FRAMES_PER_PACKET { + return Err(Error::InvalidPacket); + } + let data_start = cursor + .checked_add(header_size) + .ok_or(Error::InternalError)?; + extensions.push(PacketExtension { + id, + frame: u8::try_from(current_frame).map_err(|_| Error::InvalidPacket)?, + data: data[data_start..next].to_vec(), + }); + } else if id == EXTENSION_FRAME_SEPARATOR_ID { + if len_flag == 0 { + current_frame += 1; + } else if cursor + 1 < data.len() { + current_frame += usize::from(data[cursor + 1]); + } + if current_frame >= MAX_FRAMES_PER_PACKET { + return Err(Error::InvalidPacket); + } + } + cursor = next; + } + + Ok(extensions) +} + +#[cfg(opus_codec_frame_bounded_extensions)] +#[derive(Debug)] +struct ExtensionIteratorFrameBounded<'a> { + data: &'a [u8], + nb_frames: usize, + curr_pos: usize, + repeat_data: usize, + repeat_pos: usize, + repeat_end: usize, + repeat_frame: usize, + curr_frame: usize, + repeat_len_flag: u8, + trailing_short_len: usize, + last_long_end: Option, +} + +#[cfg(opus_codec_frame_bounded_extensions)] +impl<'a> ExtensionIteratorFrameBounded<'a> { + fn new(data: &'a [u8], nb_frames: usize) -> Self { + Self { + data, + nb_frames, + curr_pos: 0, + repeat_data: 0, + repeat_pos: 0, + repeat_end: 0, + repeat_frame: 0, + curr_frame: 0, + repeat_len_flag: 0, + trailing_short_len: 0, + last_long_end: None, + } + } + + fn next_extension(&mut self) -> Result> { + if self.nb_frames > MAX_FRAMES_PER_PACKET { + return Err(Error::InvalidPacket); + } + loop { + if let Some(extension) = self.next_repeated_extension()? { + return Ok(Some(extension)); + } + if self.curr_frame >= self.nb_frames { + return Ok(None); + } + while self.curr_pos < self.data.len() { + let start = self.curr_pos; + let entry = self.data[start]; + let id = entry >> 1; + let len_flag = entry & 1; + let (next, header_size) = skip_extension_frame_bounded(self.data, start)?; + self.curr_pos = next; + + if id == EXTENSION_FRAME_SEPARATOR_ID { + if len_flag == 0 { + self.curr_frame += 1; + } else if self.data[start + 1] != 0 { + self.curr_frame += usize::from(self.data[start + 1]); + } else { + continue; + } + if self.curr_frame >= self.nb_frames { + return Err(Error::InvalidPacket); + } + self.repeat_data = self.curr_pos; + self.last_long_end = None; + self.trailing_short_len = 0; + } else if id == EXTENSION_REPEAT_ID { + self.repeat_len_flag = len_flag; + self.repeat_frame = self.curr_frame + 1; + self.repeat_end = start; + self.repeat_pos = self.repeat_data; + break; + } else if id >= EXTENSION_FRAME_BOUNDED_MIN_DATA_ID { + if id >= EXTENSION_SHORT_ID_MAX { + self.last_long_end = Some(self.curr_pos); + self.trailing_short_len = 0; + } else { + self.trailing_short_len = self + .trailing_short_len + .checked_add(usize::from(len_flag)) + .ok_or(Error::InternalError)?; + } + let data_start = start.checked_add(header_size).ok_or(Error::InternalError)?; + return Ok(Some(PacketExtension { + id, + frame: u8::try_from(self.curr_frame).map_err(|_| Error::InvalidPacket)?, + data: self.data[data_start..next].to_vec(), + })); + } + } + if self.curr_pos >= self.data.len() { + return Ok(None); + } + } + } + + fn next_repeated_extension(&mut self) -> Result> { + while self.repeat_frame > 0 { + while self.repeat_frame < self.nb_frames { + while self.repeat_pos < self.repeat_end { + let original_id_byte = self.data[self.repeat_pos]; + let (source_next, _) = + skip_extension_frame_bounded(self.data, self.repeat_pos)?; + self.repeat_pos = source_next; + + if original_id_byte <= 3 { + continue; + } + + let mut repeated_id_byte = original_id_byte; + if self.repeat_len_flag == 0 + && self.repeat_frame + 1 >= self.nb_frames + && Some(self.repeat_pos) == self.last_long_end + { + repeated_id_byte &= !1; + } + + let payload_start = self.curr_pos; + let (payload_next, header_size) = skip_extension_payload_frame_bounded( + self.data, + self.curr_pos, + repeated_id_byte, + self.trailing_short_len, + )?; + self.curr_pos = payload_next; + let data_start = payload_start + .checked_add(header_size) + .ok_or(Error::InternalError)?; + return Ok(Some(PacketExtension { + id: repeated_id_byte >> 1, + frame: u8::try_from(self.repeat_frame).map_err(|_| Error::InvalidPacket)?, + data: self.data[data_start..payload_next].to_vec(), + })); + } + self.repeat_pos = self.repeat_data; + self.repeat_frame += 1; + } + + self.repeat_data = self.curr_pos; + self.last_long_end = None; + if self.repeat_len_flag == 0 { + self.curr_frame += 1; + if self.curr_frame >= self.nb_frames { + self.curr_pos = self.data.len(); + } + } + self.repeat_frame = 0; + } + Ok(None) + } +} + +#[cfg(opus_codec_frame_bounded_extensions)] +fn parse_extensions(data: &[u8], frame_count: usize) -> Result> { + let mut iterator = ExtensionIteratorFrameBounded::new(data, frame_count); + let mut extensions = Vec::new(); + while let Some(extension) = iterator.next_extension()? { + extensions.push(extension); + } + Ok(extensions) +} + +fn ensure_extension_space(out: &[u8], len_limit: usize, additional: usize) -> Result<()> { + let required = out + .len() + .checked_add(additional) + .ok_or(Error::InternalError)?; + if required > len_limit { + return Err(Error::BufferTooSmall); + } + Ok(()) +} + +fn write_extension_frame_separator( + out: &mut Vec, + len_limit: usize, + current_frame: &mut usize, + frame: usize, +) -> Result<()> { + if frame == *current_frame { + return Ok(()); + } + + let diff = frame + .checked_sub(*current_frame) + .ok_or(Error::InvalidPacket)?; + if diff == 1 { + ensure_extension_space(out, len_limit, 1)?; + out.push(EXTENSION_ONE_FRAME_SEPARATOR); + } else { + ensure_extension_space(out, len_limit, 2)?; + out.push(EXTENSION_MULTI_FRAME_SEPARATOR); + out.push(u8::try_from(diff).map_err(|_| Error::InvalidPacket)?); + } + *current_frame = frame; + Ok(()) +} + +fn write_extension_payload( + out: &mut Vec, + len_limit: usize, + extension: &PacketExtension, + last: bool, +) -> Result<()> { + if extension.id < EXTENSION_SHORT_ID_MAX { + if extension.data.len() > 1 { + return Err(Error::InvalidPacket); + } + ensure_extension_space(out, len_limit, extension.data.len())?; + out.extend_from_slice(&extension.data); + } else { + let length_bytes = if last { + 0 + } else { + 1 + extension.data.len() / EXTENSION_LENGTH_CHUNK + }; + ensure_extension_space(out, len_limit, length_bytes + extension.data.len())?; + if !last { + out.extend(std::iter::repeat_n( + EXTENSION_LENGTH_CHUNK_BYTE, + extension.data.len() / EXTENSION_LENGTH_CHUNK, + )); + out.push( + u8::try_from(extension.data.len() % EXTENSION_LENGTH_CHUNK) + .map_err(|_| Error::InternalError)?, + ); + } + out.extend_from_slice(&extension.data); + } + Ok(()) +} + +fn write_extension_entry( + out: &mut Vec, + len_limit: usize, + extension: &PacketExtension, + last: bool, +) -> Result<()> { + ensure_extension_space(out, len_limit, 1)?; + let len_flag = if extension.id < EXTENSION_SHORT_ID_MAX { + u8::try_from(extension.data.len()).map_err(|_| Error::InternalError)? + } else { + u8::from(!last) + }; + out.push(extension.id << 1 | len_flag); + write_extension_payload(out, len_limit, extension, last) +} + +#[cfg(not(opus_codec_frame_bounded_extensions))] +pub(super) fn generate_extensions( + extensions: &[PacketExtension], + len_limit: usize, + _frame_count: usize, +) -> Result> { + if extensions.is_empty() { + return Ok(Vec::new()); + } + + let mut max_frame = 0usize; + for ext in extensions { + let frame = usize::from(ext.frame); + if ext.id < EXTENSION_MIN_DATA_ID + || ext.id > EXTENSION_MAX_ID + || frame >= MAX_FRAMES_PER_PACKET + { + return Err(Error::InvalidPacket); + } + max_frame = max_frame.max(frame); + } + + let mut out = Vec::new(); + let mut current_frame = 0usize; + let mut written = 0usize; + + for frame in 0..=max_frame { + for ext in extensions + .iter() + .filter(|ext| usize::from(ext.frame) == frame) + { + write_extension_frame_separator(&mut out, len_limit, &mut current_frame, frame)?; + write_extension_entry(&mut out, len_limit, ext, written + 1 == extensions.len())?; + written += 1; + } + } + + Ok(out) +} + +#[cfg(opus_codec_frame_bounded_extensions)] +#[derive(Clone, Copy, Debug, Default)] +struct FrameBoundedRepeatPlan { + trigger_index: Option, + repeat_count: usize, + last_long_idx: Option, +} + +#[cfg(opus_codec_frame_bounded_extensions)] +struct FrameBoundedExtensionWriter<'a> { + extensions: &'a [PacketExtension], + len_limit: usize, + extension_count: usize, + frame_count: usize, + frame_min_idx: Vec, + frame_max_idx: Vec, + frame_repeat_idx: Vec, + out: Vec, + current_frame: usize, + written: usize, +} + +#[cfg(opus_codec_frame_bounded_extensions)] +impl<'a> FrameBoundedExtensionWriter<'a> { + fn new( + extensions: &'a [PacketExtension], + len_limit: usize, + frame_count: usize, + ) -> Result { + if frame_count > MAX_FRAMES_PER_PACKET { + return Err(Error::InvalidPacket); + } + + let extension_count = extensions.len(); + let mut frame_min_idx = vec![extension_count; frame_count]; + let mut frame_max_idx = vec![0usize; frame_count]; + for (index, extension) in extensions.iter().enumerate() { + let frame = usize::from(extension.frame); + if frame >= frame_count + || extension.id < EXTENSION_FRAME_BOUNDED_MIN_DATA_ID + || extension.id > EXTENSION_MAX_ID + { + return Err(Error::InvalidPacket); + } + frame_min_idx[frame] = frame_min_idx[frame].min(index); + frame_max_idx[frame] = frame_max_idx[frame].max(index + 1); + } + + let frame_repeat_idx = frame_min_idx.clone(); + Ok(Self { + extensions, + len_limit, + extension_count, + frame_count, + frame_min_idx, + frame_max_idx, + frame_repeat_idx, + out: Vec::new(), + current_frame: 0, + written: 0, + }) + } + + fn into_bytes(mut self) -> Result> { + for frame in 0..self.frame_count { + self.write_frame(frame)?; + } + + debug_assert_eq!(self.written, self.extension_count); + Ok(self.out) + } + + fn write_frame(&mut self, frame: usize) -> Result<()> { + let repeat_plan = self.repeat_plan(frame); + if self.frame_min_idx[frame] >= self.frame_max_idx[frame] { + return Ok(()); + } + + for index in self.frame_min_idx[frame]..self.frame_max_idx[frame] { + if usize::from(self.extensions[index].frame) != frame { + continue; + } + + self.write_extension(index)?; + if repeat_plan.trigger_index == Some(index) { + self.write_repeated_payloads(frame, repeat_plan)?; + } + } + Ok(()) + } + + fn write_extension(&mut self, index: usize) -> Result<()> { + write_extension_frame_separator( + &mut self.out, + self.len_limit, + &mut self.current_frame, + usize::from(self.extensions[index].frame), + )?; + write_extension_entry( + &mut self.out, + self.len_limit, + &self.extensions[index], + self.written + 1 == self.extension_count, + )?; + self.written += 1; + Ok(()) + } + + fn repeat_plan(&mut self, frame: usize) -> FrameBoundedRepeatPlan { + let mut plan = FrameBoundedRepeatPlan::default(); + if frame + 1 >= self.frame_count || self.frame_min_idx[frame] >= self.frame_max_idx[frame] { + return plan; + } + + for index in self.frame_min_idx[frame]..self.frame_max_idx[frame] { + if usize::from(self.extensions[index].frame) != frame { + continue; + } + if !self.extension_repeats_to_final_frame(frame, index) { + break; + } + + if self.extensions[index].id >= EXTENSION_SHORT_ID_MAX { + plan.last_long_idx = Some(self.frame_repeat_idx[self.frame_count - 1]); + } + + self.advance_future_repeat_indexes(frame); + plan.repeat_count += 1; + plan.trigger_index = Some(index); + } + plan + } + + fn extension_repeats_to_final_frame(&self, frame: usize, index: usize) -> bool { + let mut future_frame = frame + 1; + while future_frame < self.frame_count { + if self.frame_repeat_idx[future_frame] >= self.frame_max_idx[future_frame] { + break; + } + let repeated = &self.extensions[self.frame_repeat_idx[future_frame]]; + if repeated.id != self.extensions[index].id { + break; + } + if repeated.id < EXTENSION_SHORT_ID_MAX + && repeated.data.len() != self.extensions[index].data.len() + { + break; + } + future_frame += 1; + } + future_frame == self.frame_count + } + + fn advance_future_repeat_indexes(&mut self, frame: usize) { + for future_frame in (frame + 1)..self.frame_count { + let mut next = self.frame_repeat_idx[future_frame] + 1; + while next < self.frame_max_idx[future_frame] + && usize::from(self.extensions[next].frame) != future_frame + { + next += 1; + } + self.frame_repeat_idx[future_frame] = next; + } + } + + fn write_repeated_payloads( + &mut self, + frame: usize, + plan: FrameBoundedRepeatPlan, + ) -> Result<()> { + let Some(trigger_index) = plan.trigger_index else { + return Ok(()); + }; + let repeated_count = plan + .repeat_count + .checked_mul(self.frame_count - (frame + 1)) + .ok_or(Error::InternalError)?; + let last = self + .written + .checked_add(repeated_count) + .ok_or(Error::InternalError)? + == self.extension_count + || (plan.last_long_idx.is_none() && trigger_index + 1 >= self.frame_max_idx[frame]); + ensure_extension_space(&self.out, self.len_limit, 1)?; + self.out.push(EXTENSION_REPEAT_ID << 1 | u8::from(!last)); + + for future_frame in (frame + 1)..self.frame_count { + self.write_repeated_frame_payloads(future_frame, plan.last_long_idx, last)?; + } + if last { + self.current_frame += 1; + } + Ok(()) + } + + fn write_repeated_frame_payloads( + &mut self, + future_frame: usize, + last_long_idx: Option, + last: bool, + ) -> Result<()> { + let mut next = self.frame_min_idx[future_frame]; + while next < self.frame_repeat_idx[future_frame] { + if usize::from(self.extensions[next].frame) == future_frame { + write_extension_payload( + &mut self.out, + self.len_limit, + &self.extensions[next], + last && Some(next) == last_long_idx, + )?; + self.written += 1; + } + next += 1; + } + self.frame_min_idx[future_frame] = next; + Ok(()) + } +} + +#[cfg(opus_codec_frame_bounded_extensions)] +pub(super) fn generate_extensions( + extensions: &[PacketExtension], + len_limit: usize, + frame_count: usize, +) -> Result> { + if extensions.is_empty() { + return Ok(Vec::new()); + } + FrameBoundedExtensionWriter::new(extensions, len_limit, frame_count)?.into_bytes() +} + +pub(super) fn parse_existing_extensions( + data: &[u8], + frame_count: usize, +) -> Result> { + // Upstream repacketizer code treats extension-parse failures inside an + // otherwise successfully parsed packet as OPUS_INTERNAL_ERROR. + parse_extensions(data, frame_count).map_err(|_| Error::InternalError) +} diff --git a/src/projection.rs b/src/projection.rs index 0318366..eb98132 100644 --- a/src/projection.rs +++ b/src/projection.rs @@ -10,7 +10,7 @@ use crate::bindings::{ opus_projection_decoder_get_size, opus_projection_decoder_init, opus_projection_encode, opus_projection_encode_float, opus_projection_encoder_ctl, opus_projection_encoder_destroy, }; -use crate::constants::max_frame_samples_for; +use crate::constants::{is_frame_size_2_5ms_aligned, max_frame_samples_for}; use crate::error::{Error, Result}; use crate::types::{Application, Bitrate, SampleRate}; use crate::{AlignedBuffer, Ownership, RawHandle}; @@ -168,7 +168,10 @@ impl ProjectionEncoder { } fn ensure_pcm_layout(&self, len: usize, frame_size_per_ch: usize) -> Result<()> { - if len != frame_size_per_ch * self.channels as usize { + let expected = frame_size_per_ch + .checked_mul(self.channels as usize) + .ok_or(Error::BadArg)?; + if len != expected { return Err(Error::BadArg); } Ok(()) @@ -367,9 +370,14 @@ impl<'a> ProjectionEncoderRef<'a> { /// # Safety /// - `ptr` must point to valid, initialized memory of at least [`ProjectionEncoder::size()`] bytes /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - `sample_rate`, `channels`, `streams`, and `coupled_streams` must exactly match + /// the encoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped /// + /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer + /// sizes with the wrong layout and then call libopus with out-of-bounds buffers. + /// /// Use [`ProjectionEncoder::init_in_place`] to initialize the memory before calling this. #[must_use] pub unsafe fn from_raw( @@ -518,6 +526,11 @@ impl ProjectionDecoder { return Err(Error::BadArg); } let matrix_len = i32::try_from(demixing_matrix.len()).map_err(|_| Error::BadArg)?; + let mut demixing_matrix = demixing_matrix.to_vec(); + // SAFETY: libopus' C ABI takes a non-const pointer despite documenting this + // parameter as input-only. Pass a mutable scratch copy so C never receives a + // mutable pointer into the caller's immutable slice. libopus copies the + // bytes before returning and does not retain this pointer. let r = unsafe { opus_projection_decoder_init( ptr, @@ -525,7 +538,7 @@ impl ProjectionDecoder { i32::from(channels), i32::from(streams), i32::from(coupled_streams), - demixing_matrix.as_ptr().cast_mut(), + demixing_matrix.as_mut_ptr(), matrix_len, ) }; @@ -551,14 +564,17 @@ impl ProjectionDecoder { return Err(Error::BadArg); } let matrix_len = i32::try_from(demixing_matrix.len()).map_err(|_| Error::BadArg)?; + let mut demixing_matrix = demixing_matrix.to_vec(); let mut err = 0i32; + // SAFETY: see comment in init_in_place; libopus copies the scratch input + // before returning and does not retain this pointer. let dec = unsafe { opus_projection_decoder_create( sample_rate as i32, i32::from(channels), i32::from(streams), i32::from(coupled_streams), - demixing_matrix.as_ptr().cast_mut(), + demixing_matrix.as_mut_ptr(), matrix_len, &raw mut err, ) @@ -586,7 +602,10 @@ impl ProjectionDecoder { } fn ensure_output_layout(&self, len: usize, frame_size_per_ch: usize) -> Result<()> { - if len != frame_size_per_ch * self.channels as usize { + let expected = frame_size_per_ch + .checked_mul(self.channels as usize) + .ok_or(Error::BadArg)?; + if len != expected { return Err(Error::BadArg); } Ok(()) @@ -607,6 +626,12 @@ impl ProjectionDecoder { ) -> Result { self.ensure_output_layout(out.len(), frame_size_per_ch)?; let frame_size = self.validate_frame_size(frame_size_per_ch)?; + // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms. + if (packet.is_empty() || fec) + && !is_frame_size_2_5ms_aligned(frame_size_per_ch, self.sample_rate) + { + return Err(Error::BadArg); + } let packet_len = if packet.is_empty() { 0 } else { @@ -647,6 +672,12 @@ impl ProjectionDecoder { ) -> Result { self.ensure_output_layout(out.len(), frame_size_per_ch)?; let frame_size = self.validate_frame_size(frame_size_per_ch)?; + // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms. + if (packet.is_empty() || fec) + && !is_frame_size_2_5ms_aligned(frame_size_per_ch, self.sample_rate) + { + return Err(Error::BadArg); + } let packet_len = if packet.is_empty() { 0 } else { @@ -703,9 +734,14 @@ impl<'a> ProjectionDecoderRef<'a> { /// # Safety /// - `ptr` must point to valid, initialized memory of at least [`ProjectionDecoder::size()`] bytes /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - `sample_rate`, `channels`, `streams`, and `coupled_streams` must exactly match + /// the decoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped /// + /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer + /// sizes with the wrong layout and then call libopus with out-of-bounds buffers. + /// /// Use [`ProjectionDecoder::init_in_place`] to initialize the memory before calling this. #[must_use] pub unsafe fn from_raw( diff --git a/src/repacketizer.rs b/src/repacketizer.rs index 07adf18..e1f1f52 100644 --- a/src/repacketizer.rs +++ b/src/repacketizer.rs @@ -6,6 +6,8 @@ use crate::bindings::{ opus_repacketizer_out, opus_repacketizer_out_range, }; use crate::error::{Error, Result}; +#[cfg(opus_codec_rust_packet_ops)] +use crate::packet; use crate::{AlignedBuffer, Ownership, RawHandle}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; @@ -64,15 +66,13 @@ impl Repacketizer { return Err(Error::BadArg); } let len_i32 = i32::try_from(packet.len()).map_err(|_| Error::BadArg)?; - self.packets.push(packet.to_vec()); - let idx = self.packets.len() - 1; - let r = - unsafe { opus_repacketizer_cat(self.rp.as_ptr(), self.packets[idx].as_ptr(), len_i32) }; + let packet = packet.to_vec(); + let r = unsafe { opus_repacketizer_cat(self.rp.as_ptr(), packet.as_ptr(), len_i32) }; if r != 0 { - self.packets.pop(); return Err(Error::from_code(r)); } // libopus stores pointers into packet data; keep owned buffers alive. + self.packets.push(packet); Ok(()) } @@ -104,20 +104,26 @@ impl Repacketizer { /// # Errors /// Returns an error if range is invalid or output buffer is too small. pub fn emit_range(&mut self, begin: i32, end: i32, out: &mut [u8]) -> Result { - if out.is_empty() { - return Err(Error::BadArg); - } if begin < 0 || end <= begin { return Err(Error::BadArg); } - let out_len_i32 = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; - let n = unsafe { - opus_repacketizer_out_range(self.rp.as_ptr(), begin, end, out.as_mut_ptr(), out_len_i32) - }; - if n < 0 { - return Err(Error::from_code(n)); + #[cfg(opus_codec_rust_packet_ops)] + let begin_index = usize::try_from(begin).map_err(|_| Error::BadArg)?; + #[cfg(opus_codec_rust_packet_ops)] + let end_index = usize::try_from(end).map_err(|_| Error::BadArg)?; + #[cfg(not(opus_codec_rust_packet_ops))] + { + self.emit_range_via_c(begin, end, out) + } + #[cfg(opus_codec_rust_packet_ops)] + if let Some((toc, frames, paddings)) = self.repacketizer_inputs()? { + if end_index > frames.len() { + return Err(Error::BadArg); + } + packet::repacketize_frames_range(toc, &frames, &paddings, begin_index, end_index, out) + } else { + self.emit_range_via_c(begin, end, out) } - usize::try_from(n).map_err(|_| Error::InternalError) } /// Emit a packet with all queued frames. @@ -125,15 +131,16 @@ impl Repacketizer { /// # Errors /// Returns an error if the output buffer is too small. pub fn emit(&mut self, out: &mut [u8]) -> Result { - if out.is_empty() { - return Err(Error::BadArg); + #[cfg(not(opus_codec_rust_packet_ops))] + { + self.emit_via_c(out) } - let out_len_i32 = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; - let n = unsafe { opus_repacketizer_out(self.rp.as_ptr(), out.as_mut_ptr(), out_len_i32) }; - if n < 0 { - return Err(Error::from_code(n)); + #[cfg(opus_codec_rust_packet_ops)] + if let Some((toc, frames, paddings)) = self.repacketizer_inputs()? { + packet::repacketize_frames(toc, &frames, &paddings, out) + } else { + self.emit_via_c(out) } - usize::try_from(n).map_err(|_| Error::InternalError) } /// Size of a repacketizer state in bytes for external allocation. @@ -166,6 +173,57 @@ impl Repacketizer { unsafe { opus_repacketizer_init(ptr) }; Ok(()) } + + #[cfg(opus_codec_rust_packet_ops)] + fn repacketizer_inputs(&self) -> Result>> { + let c_frame_count = self.len(); + if self.packets.is_empty() { + return if c_frame_count == 0 { + Err(Error::BadArg) + } else { + Ok(None) + }; + } + + let mut toc = None; + let mut frames = Vec::new(); + let mut paddings = Vec::new(); + + for packet in &self.packets { + let (packet_toc, mut packet_frames, mut packet_paddings) = + packet::packet_repacketizer_inputs(packet)?; + toc.get_or_insert(packet_toc); + frames.append(&mut packet_frames); + paddings.append(&mut packet_paddings); + } + + if frames.len() != c_frame_count { + return Ok(None); + } + + let toc = toc.ok_or(Error::BadArg)?; + Ok(Some((toc, frames, paddings))) + } + + fn emit_range_via_c(&self, begin: i32, end: i32, out: &mut [u8]) -> Result { + let out_len_i32 = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; + let n = unsafe { + opus_repacketizer_out_range(self.rp.as_ptr(), begin, end, out.as_mut_ptr(), out_len_i32) + }; + if n < 0 { + return Err(Error::from_code(n)); + } + usize::try_from(n).map_err(|_| Error::InternalError) + } + + fn emit_via_c(&self, out: &mut [u8]) -> Result { + let out_len_i32 = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; + let n = unsafe { opus_repacketizer_out(self.rp.as_ptr(), out.as_mut_ptr(), out_len_i32) }; + if n < 0 { + return Err(Error::from_code(n)); + } + usize::try_from(n).map_err(|_| Error::InternalError) + } } impl<'a> RepacketizerRef<'a> { @@ -176,8 +234,12 @@ impl<'a> RepacketizerRef<'a> { /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// - If `ptr` already contains packet pointers, their backing storage must + /// remain valid while this wrapper is used. /// /// Use [`Repacketizer::init_in_place`] to initialize the memory before calling this. + /// If packets are pushed through this wrapper, dropping it resets the raw + /// state to avoid leaving dangling pointers to the wrapper's owned buffers. #[must_use] pub unsafe fn from_raw(ptr: *mut OpusRepacketizer) -> Self { debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); @@ -205,6 +267,18 @@ impl<'a> RepacketizerRef<'a> { } } +impl Drop for RepacketizerRef<'_> { + fn drop(&mut self) { + if !self.inner.packets.is_empty() { + // Reinitialize the external C state to clear packet pointers that + // reference our `self.inner.packets` buffers, which are about to be + // freed. Without this the caller could reuse the external + // OpusRepacketizer and dereference dangling pointers. + unsafe { opus_repacketizer_init(self.inner.rp.as_ptr()) }; + } + } +} + impl Deref for RepacketizerRef<'_> { type Target = Repacketizer; diff --git a/tests/encoder_decoder.rs b/tests/encoder_decoder.rs index 6c9939b..160a5d5 100644 --- a/tests/encoder_decoder.rs +++ b/tests/encoder_decoder.rs @@ -52,6 +52,13 @@ fn test_buffer_empty() { assert_eq!(result, Err(Error::BadArg)); } +#[test] +fn test_packet_samples_rejects_empty_packet() { + let decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono).unwrap(); + let result = decoder.packet_samples(&[]); + assert_eq!(result, Err(Error::BadArg)); +} + #[test] fn test_init_in_place_alignment_checks() { let sr = SampleRate::Hz48000; diff --git a/tests/multistream.rs b/tests/multistream.rs index 2b234a7..cf01643 100644 --- a/tests/multistream.rs +++ b/tests/multistream.rs @@ -4,6 +4,7 @@ use opus_codec::max_frame_samples_for; use opus_codec::multistream::{ Mapping, MultistreamDecoder, MultistreamDecoderRef, MultistreamEncoder, MultistreamEncoderRef, }; +use opus_codec::packet::{multistream_packet_pad, multistream_packet_unpad}; use opus_codec::types::{Application, SampleRate}; #[test] @@ -143,3 +144,26 @@ fn test_init_in_place_invalid_mapping() { let err = unsafe { MultistreamDecoder::init_in_place(dec_ptr, sr, mapping) }.unwrap_err(); assert_eq!(err, Error::BadArg); } + +#[test] +fn test_multistream_packet_pad_handles_repadding_large_packet() { + let channels = 6; + let frame_size = 960; + let (mut encoder, _) = + MultistreamEncoder::new_surround(SampleRate::Hz48000, channels, 1, Application::Audio) + .unwrap(); + + let pcm = vec![0i16; frame_size * channels as usize]; + let mut packet = vec![0u8; 70_001]; + let len = encoder.encode(&pcm, frame_size, &mut packet).unwrap(); + let original = packet[..len].to_vec(); + let nb_streams = i32::from(encoder.streams()); + + multistream_packet_pad(&mut packet, len, 70_000, nb_streams).unwrap(); + multistream_packet_pad(&mut packet, 70_000, 70_001, nb_streams).unwrap(); + assert_eq!( + multistream_packet_unpad(&mut packet, 70_001, nb_streams).unwrap(), + len + ); + assert_eq!(&packet[..len], original.as_slice()); +} diff --git a/tests/packet_padding.rs b/tests/packet_padding.rs new file mode 100644 index 0000000..aba714f --- /dev/null +++ b/tests/packet_padding.rs @@ -0,0 +1,304 @@ +use opus_codec::encoder::Encoder; +use opus_codec::error::{Error, Result}; +use opus_codec::multistream::MultistreamEncoder; +use opus_codec::packet::{ + multistream_packet_pad, multistream_packet_unpad, packet_pad, packet_unpad, +}; +use opus_codec::types::{Application, Channels, SampleRate}; +use opus_codec::{OPUS_INTERNAL_ERROR, opus_multistream_packet_pad, opus_packet_pad}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +const SINGLE_RANDOM_PACKET_CASES: usize = 24; +const SINGLE_RANDOM_PACKET_STEPS: usize = 8; +const EXTENSION_PACKET_CASES: usize = 24; +const EXTENSION_PACKET_STEPS: usize = 6; +const MULTISTREAM_PACKET_CASES: usize = 22; +const MULTISTREAM_PACKET_STEPS: usize = 8; +const RANDOMIZED_PAD_PARITY_TOTAL_CASES: usize = SINGLE_RANDOM_PACKET_CASES + * SINGLE_RANDOM_PACKET_STEPS + + EXTENSION_PACKET_CASES * EXTENSION_PACKET_STEPS + + MULTISTREAM_PACKET_CASES * MULTISTREAM_PACKET_STEPS; +const _: [(); 512] = [(); RANDOMIZED_PAD_PARITY_TOTAL_CASES]; + +fn usize_below(rng: &mut impl Rng, upper_exclusive: usize) -> usize { + if upper_exclusive <= 1 { + 0 + } else { + (rng.next_u32() as usize) % upper_exclusive + } +} + +fn i16_sample(rng: &mut impl Rng) -> i16 { + rng.next_u32() as i16 +} + +fn c_pad_result(code: i32) -> Result<()> { + if code == 0 { + Ok(()) + } else { + Err(Error::from_code(code)) + } +} + +fn assert_packet_pad_matches_opus(packet: &mut [u8], len: usize, new_len: usize) { + let mut c_packet = packet.to_vec(); + let c_result = c_pad_result(unsafe { + opus_packet_pad( + c_packet.as_mut_ptr(), + i32::try_from(len).unwrap(), + i32::try_from(new_len).unwrap(), + ) + }); + let rust_result = packet_pad(packet, len, new_len); + assert_eq!(rust_result, c_result); + if rust_result.is_ok() { + assert_eq!(&packet[..new_len], &c_packet[..new_len]); + } +} + +fn assert_multistream_packet_pad_matches_opus( + packet: &mut [u8], + len: usize, + new_len: usize, + nb_streams: i32, +) { + let mut c_packet = packet.to_vec(); + let c_result = c_pad_result(unsafe { + opus_multistream_packet_pad( + c_packet.as_mut_ptr(), + i32::try_from(len).unwrap(), + i32::try_from(new_len).unwrap(), + nb_streams, + ) + }); + let rust_result = multistream_packet_pad(packet, len, new_len, nb_streams); + assert_eq!(rust_result, c_result); + if rust_result.is_ok() { + assert_eq!(&packet[..new_len], &c_packet[..new_len]); + } +} + +fn packet_with_valid_extensions() -> Vec { + let payload = [ + 0x7B, 0x41, 16, 0x00, 67, 7, b'a', b'b', b'c', b'd', b'e', b'f', b'g', 200, b'u', b'v', + b'w', b'x', b'y', b'z', + ]; + let mut packet = vec![0u8; 40]; + packet[..payload.len()].copy_from_slice(&payload); + packet +} + +fn packet_with_malformed_extension_len() -> Vec { + let payload = [ + 0x7B, 0x41, 16, 0x00, 67, 255, b'a', b'b', b'c', b'd', b'e', b'f', b'g', 200, b'u', b'v', + b'w', b'x', b'y', b'z', + ]; + let mut packet = vec![0u8; 40]; + packet[..payload.len()].copy_from_slice(&payload); + packet +} + +fn packet_with_short_id_two_extension_in_padding() -> Vec { + let payload = [0x7B, 0x41, 2, 0x00, 0x05, b'a']; + let mut packet = vec![0u8; 10]; + packet[..payload.len()].copy_from_slice(&payload); + packet +} + +#[test] +fn packet_pad_handles_repadding_large_packet() { + let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); + let pcm = vec![0i16; 960]; + let mut encoded_packet = [0u8; 512]; + let encoded_len = encoder.encode(&pcm, &mut encoded_packet).unwrap(); + + let mut packet = vec![0u8; 70_001]; + packet[..encoded_len].copy_from_slice(&encoded_packet[..encoded_len]); + let original = packet[..encoded_len].to_vec(); + + packet_pad(&mut packet, encoded_len, 70_000).unwrap(); + packet_pad(&mut packet, 70_000, 70_001).unwrap(); + assert_eq!(packet_unpad(&mut packet, 70_001).unwrap(), encoded_len); + assert_eq!(&packet[..encoded_len], original.as_slice()); +} + +#[test] +fn packet_pad_rejects_zero_len_even_when_noop() { + let mut packet = [0u8; 1]; + let err = packet_pad(&mut packet, 0, 0).unwrap_err(); + assert_eq!(err, Error::BadArg); +} + +#[test] +fn packet_unpad_rejects_zero_len() { + let mut packet = [0u8; 1]; + let err = packet_unpad(&mut packet, 0).unwrap_err(); + assert_eq!(err, Error::BadArg); +} + +#[test] +fn packet_pad_matches_opus_for_existing_extensions() { + let mut packet = packet_with_valid_extensions(); + assert_packet_pad_matches_opus(&mut packet, 20, 40); +} + +#[test] +fn packet_pad_matches_opus_internal_error_for_malformed_extensions() { + let mut packet = packet_with_malformed_extension_len(); + let mut c_packet = packet.clone(); + + let c_result = unsafe { opus_packet_pad(c_packet.as_mut_ptr(), 20, 40) }; + assert_eq!(c_result, OPUS_INTERNAL_ERROR); + assert_eq!(packet_pad(&mut packet, 20, 40), Err(Error::InternalError)); +} + +#[test] +fn packet_pad_matches_opus_for_short_id_two_extension_in_padding() { + let mut packet = packet_with_short_id_two_extension_in_padding(); + assert_packet_pad_matches_opus(&mut packet, 6, 10); +} + +#[test] +fn packet_pad_matches_opus_randomized_repadding() { + let mut rng = StdRng::seed_from_u64(0x5EED_CAFE); + let frame_sizes = [120usize, 240, 480, 960, 1920, 2880]; + + for _ in 0..SINGLE_RANDOM_PACKET_CASES { + let frame_size = frame_sizes[usize_below(&mut rng, frame_sizes.len())]; + let mut encoder = + Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); + let pcm: Vec = (0..frame_size).map(|_| i16_sample(&mut rng)).collect(); + let mut encoded_packet = [0u8; 1500]; + let encoded_len = encoder.encode(&pcm, &mut encoded_packet).unwrap(); + + let mut packet = vec![0u8; encoded_len + 1024]; + packet[..encoded_len].copy_from_slice(&encoded_packet[..encoded_len]); + let mut len = encoded_len; + + for _ in 0..SINGLE_RANDOM_PACKET_STEPS { + let remaining = packet.len() - len; + let growth = usize_below(&mut rng, remaining + 1); + let new_len = len + growth; + assert_packet_pad_matches_opus(&mut packet, len, new_len); + len = new_len; + } + } +} + +#[test] +fn packet_pad_matches_opus_randomized_existing_extensions() { + let mut rng = StdRng::seed_from_u64(0xA11C_E55E); + + for _ in 0..EXTENSION_PACKET_CASES { + let mut packet = vec![0u8; 160]; + let base = packet_with_valid_extensions(); + packet[..20].copy_from_slice(&base[..20]); + let mut len = 20usize; + + for _ in 0..EXTENSION_PACKET_STEPS { + let remaining = packet.len() - len; + let growth = usize_below(&mut rng, remaining + 1); + let new_len = len + growth; + assert_packet_pad_matches_opus(&mut packet, len, new_len); + len = new_len; + } + } +} + +#[test] +fn multistream_pad_rejects_invalid_stream_count() { + let mut packet = vec![0u8; 8]; + assert_eq!( + multistream_packet_pad(&mut packet, 1, 2, 0).unwrap_err(), + Error::BadArg + ); + assert_eq!( + multistream_packet_pad(&mut packet, 1, 2, i32::MIN).unwrap_err(), + Error::BadArg + ); +} + +#[test] +fn multistream_pad_rejects_zero_len_even_when_noop() { + let mut packet = [0u8; 1]; + let err = multistream_packet_pad(&mut packet, 0, 0, 1).unwrap_err(); + assert_eq!(err, Error::BadArg); +} + +#[test] +fn multistream_pad_matches_opus_for_existing_extensions() { + let first_stream = [0x00, 0x01, 0x00]; + let second_stream = &packet_with_valid_extensions()[..20]; + let mut packet = vec![0u8; 50]; + packet[..first_stream.len()].copy_from_slice(&first_stream); + packet[first_stream.len()..first_stream.len() + second_stream.len()] + .copy_from_slice(second_stream); + assert_multistream_packet_pad_matches_opus(&mut packet, 23, 50, 2); +} + +#[test] +fn multistream_pad_matches_opus_internal_error_for_malformed_extensions() { + let mut packet = packet_with_malformed_extension_len(); + let mut c_packet = packet.clone(); + + let c_result = unsafe { opus_multistream_packet_pad(c_packet.as_mut_ptr(), 20, 40, 1) }; + assert_eq!(c_result, OPUS_INTERNAL_ERROR); + assert_eq!( + multistream_packet_pad(&mut packet, 20, 40, 1), + Err(Error::InternalError) + ); +} + +#[test] +fn multistream_unpad_rejects_invalid_stream_count() { + let mut packet = vec![0u8; 8]; + assert_eq!( + multistream_packet_unpad(&mut packet, 1, 0).unwrap_err(), + Error::BadArg + ); + assert_eq!( + multistream_packet_unpad(&mut packet, 1, i32::MIN).unwrap_err(), + Error::BadArg + ); +} + +#[test] +fn multistream_unpad_rejects_zero_len() { + let mut packet = [0u8; 1]; + let err = multistream_packet_unpad(&mut packet, 0, 1).unwrap_err(); + assert_eq!(err, Error::BadArg); +} + +#[test] +fn multistream_packet_pad_matches_opus_randomized_repadding() { + let mut rng = StdRng::seed_from_u64(0xC0DE_BAAD); + let frame_sizes = [120usize, 240, 480, 960]; + + for _ in 0..MULTISTREAM_PACKET_CASES { + let frame_size = frame_sizes[usize_below(&mut rng, frame_sizes.len())]; + let channels = 6usize; + let (mut encoder, _) = MultistreamEncoder::new_surround( + SampleRate::Hz48000, + channels as u8, + 1, + Application::Audio, + ) + .unwrap(); + let nb_streams = i32::from(encoder.streams()); + let pcm: Vec = (0..frame_size * channels) + .map(|_| i16_sample(&mut rng)) + .collect(); + let mut packet = vec![0u8; 4096]; + let len = encoder.encode(&pcm, frame_size, &mut packet).unwrap(); + let mut len = len; + + for _ in 0..MULTISTREAM_PACKET_STEPS { + let remaining = packet.len() - len; + let growth = usize_below(&mut rng, remaining + 1); + let new_len = len + growth; + assert_multistream_packet_pad_matches_opus(&mut packet, len, new_len, nb_streams); + len = new_len; + } + } +} diff --git a/tests/repacketizer.rs b/tests/repacketizer.rs index 4ab16e2..9006f36 100644 --- a/tests/repacketizer.rs +++ b/tests/repacketizer.rs @@ -1,9 +1,134 @@ use opus_codec::AlignedBuffer; use opus_codec::encoder::Encoder; use opus_codec::error::Error; -use opus_codec::packet::packet_frame_count; +#[cfg(not(opus_codec_rust_packet_ops))] +use opus_codec::opus_repacketizer_out_range; +use opus_codec::packet::{packet_frame_count, packet_unpad}; use opus_codec::repacketizer::{Repacketizer, RepacketizerRef}; use opus_codec::types::{Application, Channels, SampleRate}; +use opus_codec::{ + OpusRepacketizer, opus_repacketizer_cat, opus_repacketizer_create, opus_repacketizer_destroy, + opus_repacketizer_get_nb_frames, opus_repacketizer_out, +}; + +const TOC_CONFIG: u8 = 0x78; +const PACKET_CODE_1: u8 = 0x01; +const PACKET_CODE_3: u8 = 0x03; +const CODE_3_PADDING_FLAG: u8 = 0x40; +#[cfg(opus_codec_rust_packet_ops)] +const FRAME_SEPARATOR_TO_NEXT: u8 = 0x02; +const FRAME_SEPARATOR_WITH_DELTA: u8 = 0x03; +const EXTENSION_ID_5_WITH_ONE_BYTE: u8 = 0x0B; +const EXTENSION_ID_33_WITH_MISSING_LENGTH_END: u8 = 0x43; + +fn single_frame_packet(payload: u8) -> [u8; 2] { + [TOC_CONFIG, payload] +} + +fn three_frame_packet_with_extension_on_last_frame() -> [u8; 10] { + const FRAME_COUNT: u8 = 3; + const PADDING_LEN: u8 = 4; + const LAST_FRAME_INDEX: u8 = 2; + const EXTENSION_PAYLOAD: u8 = b'x'; + + [ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | FRAME_COUNT, + PADDING_LEN, + b'a', + b'b', + b'c', + FRAME_SEPARATOR_WITH_DELTA, + LAST_FRAME_INDEX, + EXTENSION_ID_5_WITH_ONE_BYTE, + EXTENSION_PAYLOAD, + ] +} + +#[cfg(opus_codec_frame_bounded_extensions)] +fn three_frame_packet_with_repeated_extension_payloads() -> [u8; 11] { + [ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | 3, + 5, + b'a', + b'b', + b'c', + EXTENSION_ID_5_WITH_ONE_BYTE, + b'x', + 0x04, + b'y', + b'z', + ] +} + +fn two_frame_packet_with_malformed_extension_on_first_frame() -> [u8; 7] { + const FRAME_COUNT: u8 = 2; + const PADDING_LEN: u8 = 2; + + [ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | FRAME_COUNT, + PADDING_LEN, + b'a', + b'b', + EXTENSION_ID_33_WITH_MISSING_LENGTH_END, + 255, + ] +} + +fn raw_repacketizer_emit(packets: &[&[u8]]) -> Result, Error> { + let rp = unsafe { opus_repacketizer_create() }; + assert!(!rp.is_null()); + + for packet in packets { + let result = unsafe { opus_repacketizer_cat(rp, packet.as_ptr(), packet.len() as i32) }; + assert_eq!(result, 0); + } + + let mut out = [0u8; 128]; + let len = unsafe { opus_repacketizer_out(rp, out.as_mut_ptr(), out.len() as i32) }; + unsafe { opus_repacketizer_destroy(rp) }; + if len < 0 { + return Err(Error::from_code(len)); + } + Ok(out[..usize::try_from(len).unwrap()].to_vec()) +} + +#[cfg(not(opus_codec_rust_packet_ops))] +fn raw_repacketizer_emit_range(packets: &[&[u8]], begin: i32, end: i32) -> Result, Error> { + let rp = unsafe { opus_repacketizer_create() }; + assert!(!rp.is_null()); + + for packet in packets { + let result = unsafe { opus_repacketizer_cat(rp, packet.as_ptr(), packet.len() as i32) }; + assert_eq!(result, 0); + } + + let mut out = [0u8; 128]; + let len = + unsafe { opus_repacketizer_out_range(rp, begin, end, out.as_mut_ptr(), out.len() as i32) }; + unsafe { opus_repacketizer_destroy(rp) }; + if len < 0 { + return Err(Error::from_code(len)); + } + Ok(out[..usize::try_from(len).unwrap()].to_vec()) +} + +#[cfg(not(opus_codec_rust_packet_ops))] +fn assert_repacketizer_result_matches_raw( + actual: Result, + out: &[u8], + expected: Result, Error>, +) { + match expected { + Ok(expected) => { + let actual_len = actual.unwrap(); + assert_eq!(&out[..actual_len], expected.as_slice()); + } + Err(expected) => assert_eq!(actual.unwrap_err(), expected), + } +} #[test] fn test_repacketizer() { @@ -81,3 +206,350 @@ fn test_repacketizer_owns_packet_data() { let out_len = rp.emit(&mut out).unwrap(); assert_eq!(&out[..out_len], original.as_slice()); } + +#[test] +fn test_repacketizer_emit_handles_padding_extensions() { + let packet = [ + 0x7B, 0x41, 16, 0x00, 67, 7, b'a', b'b', b'c', b'd', b'e', b'f', b'g', 200, b'u', b'v', + b'w', b'x', b'y', b'z', + ]; + let mut rp = Repacketizer::new().unwrap(); + rp.push(&packet).unwrap(); + + let mut out = [0u8; 64]; + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(packet_frame_count(&out[..out_len]).unwrap(), 1); + assert_eq!(&out[..out_len], packet); + + let unpadded_len = packet_unpad(&mut out, out_len).unwrap(); + assert!(unpadded_len > 0); +} + +#[test] +fn test_repacketizer_empty_state_rejects_emit() { + let mut rp = Repacketizer::new().unwrap(); + assert!(rp.is_empty()); + + let mut out = [0u8; 8]; + assert_eq!(rp.emit(&mut out).unwrap_err(), Error::BadArg); + assert_eq!(rp.emit_range(0, 1, &mut out).unwrap_err(), Error::BadArg); + + let mut empty = []; + assert_eq!(rp.emit(&mut empty).unwrap_err(), Error::BadArg); + assert_eq!(rp.emit_range(0, 1, &mut empty).unwrap_err(), Error::BadArg); +} + +#[test] +fn test_repacketizer_emit_range_validates_bounds() { + let mut rp = Repacketizer::new().unwrap(); + let packet_a = single_frame_packet(b'a'); + let packet_b = single_frame_packet(b'b'); + rp.push(&packet_a).unwrap(); + rp.push(&packet_b).unwrap(); + + let mut out = [0u8; 8]; + assert_eq!(rp.emit_range(-1, 1, &mut out).unwrap_err(), Error::BadArg); + assert_eq!(rp.emit_range(1, 1, &mut out).unwrap_err(), Error::BadArg); + assert_eq!(rp.emit_range(2, 1, &mut out).unwrap_err(), Error::BadArg); + assert_eq!(rp.emit_range(0, 3, &mut out).unwrap_err(), Error::BadArg); +} + +#[test] +fn test_repacketizer_emit_reports_buffer_too_small() { + let mut rp = Repacketizer::new().unwrap(); + let packet = single_frame_packet(b'a'); + rp.push(&packet).unwrap(); + + let mut tiny = [0u8; 1]; + assert_eq!(rp.emit(&mut tiny).unwrap_err(), Error::BufferTooSmall); + assert_eq!( + rp.emit_range(0, 1, &mut tiny).unwrap_err(), + Error::BufferTooSmall + ); + + let mut empty = []; + assert_eq!(rp.emit(&mut empty).unwrap_err(), Error::BufferTooSmall); + assert_eq!( + rp.emit_range(0, 1, &mut empty).unwrap_err(), + Error::BufferTooSmall + ); +} + +#[test] +fn test_repacketizer_emit_range_outputs_exact_frame_subset() { + let mut rp = Repacketizer::new().unwrap(); + let packet_a = single_frame_packet(b'a'); + let packet_b = single_frame_packet(b'b'); + let packet_c = single_frame_packet(b'c'); + rp.push(&packet_a).unwrap(); + rp.push(&packet_b).unwrap(); + rp.push(&packet_c).unwrap(); + + let mut out = [0u8; 16]; + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!( + &out[..out_len], + &[TOC_CONFIG | PACKET_CODE_3, 3, b'a', b'b', b'c'] + ); + + let out_len = rp.emit_range(1, 2, &mut out).unwrap(); + assert_eq!(&out[..out_len], &[TOC_CONFIG, b'b']); + + let out_len = rp.emit_range(1, 3, &mut out).unwrap(); + assert_eq!(&out[..out_len], &[TOC_CONFIG | PACKET_CODE_1, b'b', b'c']); +} + +#[cfg(opus_codec_frame_bounded_extensions)] +#[test] +fn test_repacketizer_emit_handles_repeated_extensions() { + let packet = three_frame_packet_with_repeated_extension_payloads(); + let mut rp = Repacketizer::new().unwrap(); + rp.push(&packet).unwrap(); + + let mut out = [0u8; 32]; + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], packet); + + let out_len = rp.emit_range(1, 3, &mut out).unwrap(); + assert_eq!( + &out[..out_len], + &[ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | 2, + 4, + b'b', + b'c', + EXTENSION_ID_5_WITH_ONE_BYTE, + b'y', + 0x04, + b'z', + ] + ); +} + +#[test] +fn test_repacketizer_emit_range_filters_and_reindexes_extensions() { + let mut rp = Repacketizer::new().unwrap(); + let packet = three_frame_packet_with_extension_on_last_frame(); + rp.push(&packet).unwrap(); + + let mut out = [0u8; 32]; + #[cfg(opus_codec_rust_packet_ops)] + { + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], packet); + + let out_len = rp.emit_range(0, 1, &mut out).unwrap(); + assert_eq!(&out[..out_len], &[TOC_CONFIG, b'a']); + + let out_len = rp.emit_range(1, 3, &mut out).unwrap(); + assert_eq!( + &out[..out_len], + &[ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | 2, + 3, + b'b', + b'c', + FRAME_SEPARATOR_TO_NEXT, + EXTENSION_ID_5_WITH_ONE_BYTE, + b'x', + ] + ); + + let out_len = rp.emit_range(2, 3, &mut out).unwrap(); + assert_eq!( + &out[..out_len], + &[ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | 1, + 2, + b'c', + EXTENSION_ID_5_WITH_ONE_BYTE, + b'x', + ] + ); + } + #[cfg(not(opus_codec_rust_packet_ops))] + { + assert_repacketizer_result_matches_raw( + rp.emit(&mut out), + &out, + raw_repacketizer_emit(&[&packet]), + ); + + assert_repacketizer_result_matches_raw( + rp.emit_range(0, 1, &mut out), + &out, + raw_repacketizer_emit_range(&[&packet], 0, 1), + ); + + assert_repacketizer_result_matches_raw( + rp.emit_range(1, 3, &mut out), + &out, + raw_repacketizer_emit_range(&[&packet], 1, 3), + ); + + assert_repacketizer_result_matches_raw( + rp.emit_range(2, 3, &mut out), + &out, + raw_repacketizer_emit_range(&[&packet], 2, 3), + ); + } +} + +#[test] +fn test_repacketizer_emit_range_ignores_malformed_extensions_outside_range() { + let mut rp = Repacketizer::new().unwrap(); + let packet = two_frame_packet_with_malformed_extension_on_first_frame(); + rp.push(&packet).unwrap(); + + let mut out = [0u8; 8]; + assert_eq!( + rp.emit_range(0, 1, &mut out).unwrap_err(), + Error::InternalError + ); + + let out_len = rp.emit_range(1, 2, &mut out).unwrap(); + assert_eq!(&out[..out_len], &[TOC_CONFIG, b'b']); +} + +#[test] +fn test_repacketizer_emit_sorts_extensions_by_output_frame() { + let future_frame_extension = [ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | 1, + 4, + b'a', + FRAME_SEPARATOR_WITH_DELTA, + 2, + EXTENSION_ID_5_WITH_ONE_BYTE, + b'x', + ]; + let current_frame_extension = [ + TOC_CONFIG | PACKET_CODE_3, + CODE_3_PADDING_FLAG | 1, + 2, + b'b', + EXTENSION_ID_5_WITH_ONE_BYTE, + b'y', + ]; + let plain_packet = single_frame_packet(b'c'); + let expected = raw_repacketizer_emit(&[ + &future_frame_extension, + ¤t_frame_extension, + &plain_packet, + ]); + + let mut rp = Repacketizer::new().unwrap(); + rp.push(&future_frame_extension).unwrap(); + rp.push(¤t_frame_extension).unwrap(); + rp.push(&plain_packet).unwrap(); + + let mut out = [0u8; 128]; + match expected { + Ok(expected) => { + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], expected.as_slice()); + } + Err(expected) => { + assert_eq!(rp.emit(&mut out).unwrap_err(), expected); + } + } +} + +#[test] +fn test_repacketizer_reset_clears_state_and_allows_reuse() { + let mut rp = Repacketizer::new().unwrap(); + let packet_a = single_frame_packet(b'a'); + let packet_b = single_frame_packet(b'b'); + rp.push(&packet_a).unwrap(); + rp.push(&packet_b).unwrap(); + assert_eq!(rp.len(), 2); + + rp.reset(); + assert!(rp.is_empty()); + let mut out = [0u8; 8]; + assert_eq!(rp.emit(&mut out).unwrap_err(), Error::BadArg); + + rp.push(&packet_b).unwrap(); + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], packet_b); +} + +#[test] +fn test_repacketizer_failed_push_does_not_change_state() { + let mut rp = Repacketizer::new().unwrap(); + let packet = single_frame_packet(b'a'); + let incompatible = [0x00, b'z']; + rp.push(&packet).unwrap(); + + assert_eq!(rp.push(&incompatible).unwrap_err(), Error::InvalidPacket); + assert_eq!(rp.len(), 1); + + let mut out = [0u8; 8]; + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], packet); +} + +#[test] +fn test_repacketizer_ref_init_in_rejects_small_buffer() { + let mut buf = AlignedBuffer::with_capacity_bytes(0); + assert!(matches!( + RepacketizerRef::init_in(&mut buf), + Err(Error::BadArg) + )); +} + +#[test] +fn test_init_in_place_rejects_misaligned_pointer() { + let required = Repacketizer::size().unwrap(); + let mut storage = vec![0u8; required + std::mem::align_of::()]; + let ptr = unsafe { storage.as_mut_ptr().add(1).cast::() }; + + let err = unsafe { Repacketizer::init_in_place(ptr) }.unwrap_err(); + assert_eq!(err, Error::BadArg); +} + +#[test] +fn test_repacketizer_ref_drop_resets_external_state() { + let rp_size = Repacketizer::size().unwrap(); + let mut rp_buf = AlignedBuffer::with_capacity_bytes(rp_size); + let rp_ptr = rp_buf.as_mut_ptr(); + unsafe { + Repacketizer::init_in_place(rp_ptr).unwrap(); + } + + { + let packet = single_frame_packet(b'a'); + let mut rp = unsafe { RepacketizerRef::from_raw(rp_ptr) }; + rp.push(&packet).unwrap(); + assert_eq!(unsafe { opus_repacketizer_get_nb_frames(rp_ptr) }, 1); + } + + assert_eq!(unsafe { opus_repacketizer_get_nb_frames(rp_ptr) }, 0); +} + +#[test] +fn test_repacketizer_ref_can_emit_prepopulated_raw_state() { + let rp_ptr = unsafe { opus_repacketizer_create() }; + assert!(!rp_ptr.is_null()); + let packet = single_frame_packet(b'a'); + let result = unsafe { opus_repacketizer_cat(rp_ptr, packet.as_ptr(), packet.len() as i32) }; + assert_eq!(result, 0); + + { + let mut rp = unsafe { RepacketizerRef::from_raw(rp_ptr) }; + assert_eq!(rp.len(), 1); + + let mut out = [0u8; 8]; + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], packet); + + let out_len = rp.emit_range(0, 1, &mut out).unwrap(); + assert_eq!(&out[..out_len], packet); + } + + assert_eq!(unsafe { opus_repacketizer_get_nb_frames(rp_ptr) }, 1); + unsafe { opus_repacketizer_destroy(rp_ptr) }; +}