Skip to content

Commit 5ce0ff4

Browse files
committed
Decode non-UTF-8 client and port names lossily
1 parent 83bf2ff commit 5ce0ff4

4 files changed

Lines changed: 89 additions & 20 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
bytes are replaced with `U+FFFD`. `PropertyChange` now implements `Clone`.
3737
- Actually stop processing if buffer size callback returns Quit. This was not
3838
respected in Pipewire.
39+
- Use lossy string decoding for non-UTF8 names. May affect reading metadata from
40+
non-Rust clients.
3941

4042
## 0.9.0
4143

‎src/client/callbacks.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,10 @@ unsafe extern "C" fn client_registration<N, P>(
352352
let Some(ctx) = CallbackContext::<N, P>::from_raw(data) else {
353353
return;
354354
};
355-
let name = ffi::CStr::from_ptr(name).to_str().unwrap();
355+
let name = ffi::CStr::from_ptr(name).to_string_lossy();
356356
let register = !matches!(register, 0);
357357
ctx.notification
358-
.client_registration(&ctx.client, name, register);
358+
.client_registration(&ctx.client, &name, register);
359359
});
360360
if let Err(err) = res {
361361
if let Some(ctx) = CallbackContext::<N, P>::from_raw(data) {
@@ -410,11 +410,11 @@ where
410410
let Some(ctx) = CallbackContext::<N, P>::from_raw(data) else {
411411
return Control::Quit;
412412
};
413-
let old_name = ffi::CStr::from_ptr(old_name).to_str().unwrap();
414-
let new_name = ffi::CStr::from_ptr(new_name).to_str().unwrap();
413+
let old_name = ffi::CStr::from_ptr(old_name).to_string_lossy();
414+
let new_name = ffi::CStr::from_ptr(new_name).to_string_lossy();
415415
let c = ctx
416416
.notification
417-
.port_rename(&ctx.client, port_id, old_name, new_name);
417+
.port_rename(&ctx.client, port_id, &old_name, &new_name);
418418
if c == Control::Quit {
419419
ctx.mark_invalid(false);
420420
}

‎src/client/client_impl.rs‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub struct Client(Arc<ClientHandle>);
3737
/// The client is closed when the last strong reference is dropped.
3838
pub struct ClientHandle {
3939
client_ptr: *mut j::jack_client_t,
40+
// The client name, decoded lossily once when the client is opened. JACK never renames an
41+
// open client, so the cached value stays accurate.
42+
name: Box<str>,
4043
// Keep callback storage alive until after jack_client_close returns.
4144
property_change_handler: Mutex<Option<Box<dyn PropertyChangeHandler>>>,
4245
}
@@ -189,12 +192,10 @@ impl Client {
189192
/// as JACK will may rename a client if necessary (ie: name collision, name too long). The name
190193
/// will only the be different than the one passed to `Client::new` if the `ClientStatus` was
191194
/// `NAME_NOT_UNIQUE`.
195+
///
196+
/// Bytes that are not valid UTF-8 are replaced with `U+FFFD REPLACEMENT CHARACTER`.
192197
pub fn name(&self) -> &str {
193-
unsafe {
194-
let ptr = j::jack_get_client_name(self.raw());
195-
let cstr = ffi::CStr::from_ptr(ptr);
196-
cstr.to_str().unwrap()
197-
}
198+
&self.0.name
198199
}
199200

200201
/// The current maximum size that will every be passed to the process
@@ -264,10 +265,7 @@ impl Client {
264265
unsafe {
265266
let uuid_s = j::jack_client_get_uuid(self.raw());
266267
assert!(!uuid_s.is_null());
267-
let uuid = ffi::CStr::from_ptr(uuid_s)
268-
.to_str()
269-
.expect("uuid is valid string")
270-
.to_string();
268+
let uuid = ffi::CStr::from_ptr(uuid_s).to_string_lossy().into_owned();
271269
j::jack_free(uuid_s as _);
272270
uuid
273271
}
@@ -280,12 +278,7 @@ impl Client {
280278
if name_ptr.is_null() {
281279
None
282280
} else {
283-
Some(
284-
ffi::CStr::from_ptr(name_ptr)
285-
.to_str()
286-
.expect("name convert to a valid str")
287-
.to_string(),
288-
)
281+
Some(ffi::CStr::from_ptr(name_ptr).to_string_lossy().into_owned())
289282
}
290283
}
291284
}
@@ -706,8 +699,17 @@ impl Client {
706699
/// # Safety
707700
/// It is unsafe to create a `Client` from a raw pointer.
708701
pub unsafe fn from_raw(p: *mut j::jack_client_t) -> Self {
702+
let name = unsafe {
703+
let name_ptr = j::jack_get_client_name(p);
704+
if name_ptr.is_null() {
705+
Box::default()
706+
} else {
707+
ffi::CStr::from_ptr(name_ptr).to_string_lossy().into()
708+
}
709+
};
709710
Client(Arc::new(ClientHandle {
710711
client_ptr: p,
712+
name,
711713
property_change_handler: Mutex::new(None),
712714
}))
713715
}

‎src/tests/client.rs‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,68 @@ fn client_with_unknown_server_name_fails_to_open() {
177177
err
178178
);
179179
}
180+
181+
/// Opens a client whose name is not valid UTF-8. `Client::new` only accepts `&str`, but other
182+
/// processes on the server can use any bytes.
183+
fn open_non_utf8_client(name: &[u8]) -> crate::Client {
184+
let name = std::ffi::CString::new(name).unwrap();
185+
let mut status = 0;
186+
let ptr = unsafe {
187+
jack_sys::jack_client_open(name.as_ptr(), jack_sys::JackNoStartServer, &mut status)
188+
};
189+
assert!(!ptr.is_null(), "jack_client_open failed: {status:#x}");
190+
unsafe { crate::Client::from_raw(ptr) }
191+
}
192+
193+
#[test]
194+
fn non_utf8_client_name_is_decoded_lossily() {
195+
let client = open_non_utf8_client(b"non-utf8-\xff");
196+
assert_eq!(client.name(), "non-utf8-\u{fffd}");
197+
assert_eq!(
198+
client.name_by_uuid(client.uuid()).as_deref(),
199+
Some("non-utf8-\u{fffd}")
200+
);
201+
}
202+
203+
#[test]
204+
fn non_utf8_client_registration_does_not_stop_processing() {
205+
use std::sync::atomic::{AtomicUsize, Ordering};
206+
use std::sync::{Arc, Mutex};
207+
208+
struct Registrations(Arc<Mutex<Vec<String>>>);
209+
impl crate::NotificationHandler for Registrations {
210+
fn client_registration(&mut self, _: &crate::Client, name: &str, is_registered: bool) {
211+
if is_registered {
212+
self.0.lock().unwrap().push(name.to_string());
213+
}
214+
}
215+
}
216+
217+
let names = Arc::new(Mutex::new(Vec::new()));
218+
let cycles = Arc::new(AtomicUsize::new(0));
219+
let process = {
220+
let cycles = cycles.clone();
221+
crate::contrib::ClosureProcessHandler::new(move |_, _| {
222+
cycles.fetch_add(1, Ordering::Relaxed);
223+
crate::Control::Continue
224+
})
225+
};
226+
let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap();
227+
let ac = client
228+
.activate_async(Registrations(names.clone()), process)
229+
.unwrap();
230+
std::thread::sleep(std::time::Duration::from_millis(100));
231+
232+
let _other = open_non_utf8_client(b"other-\xfe");
233+
std::thread::sleep(std::time::Duration::from_millis(200));
234+
assert!(
235+
names.lock().unwrap().iter().any(|n| n == "other-\u{fffd}"),
236+
"{:?}",
237+
names.lock().unwrap()
238+
);
239+
240+
let before = cycles.load(Ordering::Relaxed);
241+
std::thread::sleep(std::time::Duration::from_millis(200));
242+
assert!(cycles.load(Ordering::Relaxed) > before);
243+
ac.deactivate().unwrap();
244+
}

0 commit comments

Comments
 (0)