Harden dependency and supply-chain policy (#100)
Some checks failed
Native code generation / deterministic (push) Failing after 2m6s
Imaging and meshing gate / native (push) Failing after 2m48s
JPEG 2000 feature / linux (push) Successful in 2m43s
Release platform and feature matrix / audit (push) Successful in 35s
Native Rust workspace compile / compile (push) Failing after 57s
Skia feature / linux (push) Successful in 31m0s
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Some checks failed
Native code generation / deterministic (push) Failing after 2m6s
Imaging and meshing gate / native (push) Failing after 2m48s
JPEG 2000 feature / linux (push) Successful in 2m43s
Release platform and feature matrix / audit (push) Successful in 35s
Native Rust workspace compile / compile (push) Failing after 57s
Skia feature / linux (push) Successful in 31m0s
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
This commit is contained in:
28
crates/libremetaverse-opus/Cargo.toml
Normal file
28
crates/libremetaverse-opus/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "libremetaverse-opus"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Safe system-libopus adapter for MetaCrate"
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3"
|
||||
vcpkg = "0.2"
|
||||
|
||||
# cargo-machete cannot associate target-conditional build.rs references with
|
||||
# build dependencies. Both crates are invoked directly in build.rs.
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["pkg-config", "vcpkg"]
|
||||
|
||||
# Unsafe code is permitted only inside this reviewed native ABI boundary.
|
||||
# Every consumer receives an owned, validated safe Rust API.
|
||||
[lints.rust]
|
||||
unsafe_code = "allow"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
16
crates/libremetaverse-opus/build.rs
Normal file
16
crates/libremetaverse-opus/build.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
let target = env::var("TARGET").expect("Cargo provides TARGET");
|
||||
if target.contains("msvc") {
|
||||
vcpkg::Config::new()
|
||||
.find_package("opus")
|
||||
.expect("WebRTC voice requires libopus 1.3 or newer from vcpkg");
|
||||
} else {
|
||||
pkg_config::Config::new()
|
||||
.atleast_version("1.3")
|
||||
.probe("opus")
|
||||
.expect("WebRTC voice requires pkg-config and libopus 1.3 or newer");
|
||||
}
|
||||
}
|
||||
313
crates/libremetaverse-opus/src/lib.rs
Normal file
313
crates/libremetaverse-opus/src/lib.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
//! Safe, narrowly scoped access to the system `libopus` codec.
|
||||
//!
|
||||
//! The foreign ABI, raw handles, and error pointers are contained in this
|
||||
//! crate. Callers can move an encoder or decoder between threads, but all
|
||||
//! codec operations require exclusive access because libopus mutates them.
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::{CStr, c_char, c_int};
|
||||
use std::fmt;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
const OPUS_OK: c_int = 0;
|
||||
const OPUS_BAD_ARG: c_int = -1;
|
||||
const OPUS_ALLOC_FAIL: c_int = -7;
|
||||
const OPUS_APPLICATION_VOIP: c_int = 2_048;
|
||||
const MAX_PACKET_BYTES: usize = 1_275;
|
||||
|
||||
#[repr(C)]
|
||||
struct OpusEncoder {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct OpusDecoder {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
#[link(name = "opus")]
|
||||
unsafe extern "C" {
|
||||
fn opus_encoder_create(
|
||||
sample_rate: c_int,
|
||||
channels: c_int,
|
||||
application: c_int,
|
||||
error: *mut c_int,
|
||||
) -> *mut OpusEncoder;
|
||||
fn opus_encoder_destroy(encoder: *mut OpusEncoder);
|
||||
fn opus_encode(
|
||||
encoder: *mut OpusEncoder,
|
||||
pcm: *const i16,
|
||||
frame_size: c_int,
|
||||
output: *mut u8,
|
||||
max_output_bytes: c_int,
|
||||
) -> c_int;
|
||||
fn opus_decoder_create(
|
||||
sample_rate: c_int,
|
||||
channels: c_int,
|
||||
error: *mut c_int,
|
||||
) -> *mut OpusDecoder;
|
||||
fn opus_decoder_destroy(decoder: *mut OpusDecoder);
|
||||
fn opus_decode(
|
||||
decoder: *mut OpusDecoder,
|
||||
packet: *const u8,
|
||||
packet_bytes: c_int,
|
||||
pcm: *mut i16,
|
||||
frame_size: c_int,
|
||||
decode_fec: c_int,
|
||||
) -> c_int;
|
||||
fn opus_strerror(error: c_int) -> *const c_char;
|
||||
}
|
||||
|
||||
/// Channel layouts accepted by the Opus multirate API.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[repr(i32)]
|
||||
pub enum Channels {
|
||||
Mono = 1,
|
||||
Stereo = 2,
|
||||
}
|
||||
|
||||
impl Channels {
|
||||
const fn count(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable libopus error code returned by a codec operation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Error(c_int);
|
||||
|
||||
impl Error {
|
||||
#[must_use]
|
||||
pub const fn code(self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let message = unsafe {
|
||||
let pointer = opus_strerror(self.0);
|
||||
(!pointer.is_null()).then(|| CStr::from_ptr(pointer).to_string_lossy())
|
||||
};
|
||||
match message {
|
||||
Some(message) => write!(formatter, "libopus error {}: {message}", self.0),
|
||||
None => write!(formatter, "libopus error {}", self.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
/// Owned Opus `VoIP` encoder.
|
||||
pub struct Encoder {
|
||||
raw: NonNull<OpusEncoder>,
|
||||
channels: Channels,
|
||||
sample_rate: usize,
|
||||
}
|
||||
|
||||
// libopus codec handles have no thread affinity. Exclusive method access
|
||||
// prevents concurrent calls after the owned handle is moved to another thread.
|
||||
unsafe impl Send for Encoder {}
|
||||
|
||||
impl Encoder {
|
||||
/// Creates a `VoIP` encoder for a supported Opus sample rate.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the native libopus error when the configuration is unsupported
|
||||
/// or the codec cannot be allocated.
|
||||
pub fn voip(sample_rate: u32, channels: Channels) -> Result<Self, Error> {
|
||||
let sample_rate = validate_sample_rate(sample_rate)?;
|
||||
let sample_rate_usize = usize::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))?;
|
||||
let mut error = OPUS_OK;
|
||||
let raw = unsafe {
|
||||
opus_encoder_create(
|
||||
sample_rate,
|
||||
channels as c_int,
|
||||
OPUS_APPLICATION_VOIP,
|
||||
&raw mut error,
|
||||
)
|
||||
};
|
||||
let raw = NonNull::new(raw).ok_or(Error(if error == OPUS_OK {
|
||||
OPUS_ALLOC_FAIL
|
||||
} else {
|
||||
error
|
||||
}))?;
|
||||
if error != OPUS_OK {
|
||||
unsafe { opus_encoder_destroy(raw.as_ptr()) };
|
||||
return Err(Error(error));
|
||||
}
|
||||
Ok(Self {
|
||||
raw,
|
||||
channels,
|
||||
sample_rate: sample_rate_usize,
|
||||
})
|
||||
}
|
||||
|
||||
/// Encodes one complete Opus frame into a caller-owned packet buffer.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Rejects malformed channel/frame lengths and oversized output buffers,
|
||||
/// and returns any native codec failure.
|
||||
pub fn encode(&mut self, pcm: &[i16], output: &mut [u8]) -> Result<usize, Error> {
|
||||
let channels = self.channels.count();
|
||||
if !pcm.len().is_multiple_of(channels) || output.is_empty() {
|
||||
return Err(Error(OPUS_BAD_ARG));
|
||||
}
|
||||
let frame_size = pcm.len() / channels;
|
||||
let base = self.sample_rate / 400;
|
||||
if ![base, base * 2, base * 4, base * 8, base * 16, base * 24].contains(&frame_size) {
|
||||
return Err(Error(OPUS_BAD_ARG));
|
||||
}
|
||||
let result = unsafe {
|
||||
opus_encode(
|
||||
self.raw.as_ptr(),
|
||||
pcm.as_ptr(),
|
||||
c_int::try_from(frame_size).map_err(|_| Error(OPUS_BAD_ARG))?,
|
||||
output.as_mut_ptr(),
|
||||
c_int::try_from(output.len()).map_err(|_| Error(OPUS_BAD_ARG))?,
|
||||
)
|
||||
};
|
||||
result_to_size(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Encoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe { opus_encoder_destroy(self.raw.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned Opus decoder.
|
||||
pub struct Decoder {
|
||||
raw: NonNull<OpusDecoder>,
|
||||
channels: Channels,
|
||||
sample_rate: usize,
|
||||
}
|
||||
|
||||
// See the Encoder safety argument above.
|
||||
unsafe impl Send for Decoder {}
|
||||
|
||||
impl Decoder {
|
||||
/// Creates a decoder for a supported Opus sample rate.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the native libopus error when the configuration is unsupported
|
||||
/// or the codec cannot be allocated.
|
||||
pub fn new(sample_rate: u32, channels: Channels) -> Result<Self, Error> {
|
||||
let sample_rate = validate_sample_rate(sample_rate)?;
|
||||
let sample_rate_usize = usize::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))?;
|
||||
let mut error = OPUS_OK;
|
||||
let raw = unsafe { opus_decoder_create(sample_rate, channels as c_int, &raw mut error) };
|
||||
let raw = NonNull::new(raw).ok_or(Error(if error == OPUS_OK {
|
||||
OPUS_ALLOC_FAIL
|
||||
} else {
|
||||
error
|
||||
}))?;
|
||||
if error != OPUS_OK {
|
||||
unsafe { opus_decoder_destroy(raw.as_ptr()) };
|
||||
return Err(Error(error));
|
||||
}
|
||||
Ok(Self {
|
||||
raw,
|
||||
channels,
|
||||
sample_rate: sample_rate_usize,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes one Opus packet, or performs packet-loss concealment for `None`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Rejects invalid output alignment/length and packets too large for the
|
||||
/// libopus single-stream format, and returns any native codec failure.
|
||||
pub fn decode(
|
||||
&mut self,
|
||||
packet: Option<&[u8]>,
|
||||
pcm: &mut [i16],
|
||||
decode_fec: bool,
|
||||
) -> Result<usize, Error> {
|
||||
let channels = self.channels.count();
|
||||
if !pcm.len().is_multiple_of(channels)
|
||||
|| pcm.is_empty()
|
||||
|| pcm.len() / channels > self.sample_rate * 120 / 1_000
|
||||
|| packet.is_some_and(|value| value.is_empty() || value.len() > MAX_PACKET_BYTES)
|
||||
{
|
||||
return Err(Error(OPUS_BAD_ARG));
|
||||
}
|
||||
let frame_size = pcm.len() / channels;
|
||||
let (packet_pointer, packet_bytes) = packet.map_or((std::ptr::null(), 0), |value| {
|
||||
(
|
||||
value.as_ptr(),
|
||||
c_int::try_from(value.len()).unwrap_or(c_int::MAX),
|
||||
)
|
||||
});
|
||||
let result = unsafe {
|
||||
opus_decode(
|
||||
self.raw.as_ptr(),
|
||||
packet_pointer,
|
||||
packet_bytes,
|
||||
pcm.as_mut_ptr(),
|
||||
c_int::try_from(frame_size).map_err(|_| Error(OPUS_BAD_ARG))?,
|
||||
c_int::from(decode_fec),
|
||||
)
|
||||
};
|
||||
result_to_size(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Decoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe { opus_decoder_destroy(self.raw.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_sample_rate(sample_rate: u32) -> Result<c_int, Error> {
|
||||
if matches!(sample_rate, 8_000 | 12_000 | 16_000 | 24_000 | 48_000) {
|
||||
c_int::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))
|
||||
} else {
|
||||
Err(Error(OPUS_BAD_ARG))
|
||||
}
|
||||
}
|
||||
|
||||
fn result_to_size(result: c_int) -> Result<usize, Error> {
|
||||
usize::try_from(result).map_err(|_| Error(result))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn system_libopus_encodes_and_decodes_a_real_frame() {
|
||||
let mut encoder = Encoder::voip(48_000, Channels::Mono).unwrap();
|
||||
let mut decoder = Decoder::new(48_000, Channels::Mono).unwrap();
|
||||
let pcm = (0..960)
|
||||
.map(|index| if index % 40 < 20 { 8_000 } else { -8_000 })
|
||||
.collect::<Vec<_>>();
|
||||
let mut packet = [0_u8; MAX_PACKET_BYTES];
|
||||
let packet_bytes = encoder.encode(&pcm, &mut packet).unwrap();
|
||||
assert!(packet_bytes > 0);
|
||||
|
||||
let mut output_pcm = [0_i16; 960];
|
||||
assert_eq!(
|
||||
decoder
|
||||
.decode(Some(&packet[..packet_bytes]), &mut output_pcm, false)
|
||||
.unwrap(),
|
||||
960
|
||||
);
|
||||
assert!(output_pcm.iter().any(|sample| *sample != 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_boundary_rejects_invalid_shapes() {
|
||||
assert!(Encoder::voip(44_100, Channels::Mono).is_err());
|
||||
let mut encoder = Encoder::voip(48_000, Channels::Mono).unwrap();
|
||||
assert!(encoder.encode(&[0; 100], &mut [0; 100]).is_err());
|
||||
let mut decoder = Decoder::new(48_000, Channels::Stereo).unwrap();
|
||||
assert!(decoder.decode(Some(&[1]), &mut [0; 3], false).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user