use core::ffi::{c_char, c_int};
use std::ffi::{CString, OsStr};
use std::os::unix::ffi::OsStrExt;
use std::ptr;
use crate::error::Error;
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated audio system parses arguments; still unit-tested on the host"
)
)]
pub(crate) struct Arguments {
storage: Vec<CString>,
argv: Vec<*mut c_char>,
}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated audio system parses arguments; still unit-tested on the host"
)
)]
impl Arguments {
pub(crate) fn new<I, S>(args: I) -> Result<Self, Error>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let storage = args
.into_iter()
.map(|arg| CString::new(arg.as_ref().as_bytes()).map_err(|_| Error::CommandLineNul))
.collect::<Result<Vec<_>, Error>>()?;
let mut argv: Vec<*mut c_char> = storage
.iter()
.map(|arg| arg.as_ptr().cast_mut())
.collect();
argv.push(ptr::null_mut());
Ok(Self { storage, argv })
}
fn argc(&self) -> c_int {
c_int::try_from(self.storage.len()).unwrap_or(c_int::MAX)
}
fn as_argv(&mut self) -> *const *mut c_char {
self.argv.as_mut_ptr()
}
}
#[cfg(bela_device)]
pub(crate) fn parse(
arguments: &mut Arguments,
raw: &mut bela_sys::BelaInitSettings,
) -> Result<(), Error> {
let ret = unsafe {
bela_sys::Bela_getopt_long(
arguments.argc(),
arguments.as_argv(),
c"".as_ptr(),
ptr::null(),
raw,
)
};
if ret < 0 {
Ok(())
} else {
Err(Error::CommandLine(ret))
}
}
#[cfg(bela_device)]
pub fn print_usage() {
unsafe { bela_sys::Bela_usage() }
eprintln!(
" Note: --receive-port [-R], --transmit-port [-T] and --server-name [-S] appear above \
but are not implemented; passing one is an error."
);
}
#[cfg(test)]
mod tests {
use core::ffi::CStr;
use super::*;
fn as_c_sees_them(arguments: &Arguments) -> Vec<Vec<u8>> {
arguments
.argv
.iter()
.take_while(|arg| !arg.is_null())
.map(|arg| unsafe { CStr::from_ptr(*arg) }.to_bytes().to_vec())
.collect()
}
#[test]
fn arguments_are_copied_in_order() {
let mut arguments =
Arguments::new(["my-app", "--period", "64", "-v"]).expect("no NUL bytes");
assert_eq!(arguments.argc(), 4);
assert_eq!(
as_c_sees_them(&arguments),
[
b"my-app".to_vec(),
b"--period".to_vec(),
b"64".to_vec(),
b"-v".to_vec()
]
);
assert!(
!arguments.as_argv().is_null(),
"the array has to be addressable"
);
}
#[test]
fn the_pointer_array_is_null_terminated() {
let arguments = Arguments::new(["my-app", "-v"]).expect("no NUL bytes");
assert_eq!(
arguments.argv.len(),
3,
"one pointer per argument plus the terminator a C main has"
);
assert!(
arguments.argv.last().is_some_and(|last| last.is_null()),
"the array must end the way a C argv does"
);
}
#[test]
fn arguments_that_are_not_utf8_survive() {
let path = OsStr::from_bytes(b"settings-\xff.json");
let arguments = Arguments::new([OsStr::new("my-app"), OsStr::new("--json-file"), path])
.expect("no NUL bytes");
assert_eq!(
as_c_sees_them(&arguments)[2],
b"settings-\xff.json".to_vec(),
"the bytes have to reach C unchanged"
);
}
#[test]
fn an_argument_containing_a_nul_is_refused() {
assert_eq!(
Arguments::new(["my-app", "--period\0 64"]).err(),
Some(Error::CommandLineNul),
"a C string cannot carry it, and truncating would change what was asked for"
);
}
#[test]
fn an_empty_list_is_empty() {
let arguments = Arguments::new(Vec::<String>::new()).expect("no NUL bytes");
assert_eq!(arguments.argc(), 0);
assert_eq!(as_c_sees_them(&arguments), Vec::<Vec<u8>>::new());
}
}