1use std::ffi::{CString, c_char};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10pub struct DiskControlInterfaceVersion(u32);
11
12impl DiskControlInterfaceVersion {
13 pub const fn new(version: u32) -> Self {
14 Self(version)
15 }
16
17 pub const fn get(self) -> u32 {
18 self.0
19 }
20
21 pub const fn supports_extended(self) -> bool {
22 self.0 >= 1
23 }
24}
25
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub struct DiskIndex(u32);
28
29impl DiskIndex {
30 pub const fn new(index: u32) -> Self {
31 Self(index)
32 }
33
34 pub const fn as_raw(self) -> u32 {
35 self.0
36 }
37}
38
39impl From<u32> for DiskIndex {
40 fn from(index: u32) -> Self {
41 Self::new(index)
42 }
43}
44
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
46pub enum DiskTrayState {
47 #[default]
48 Closed,
49 Ejected,
50}
51
52impl DiskTrayState {
53 pub const fn from_ejected(ejected: bool) -> Self {
54 if ejected { Self::Ejected } else { Self::Closed }
55 }
56
57 pub const fn is_ejected(self) -> bool {
58 matches!(self, Self::Ejected)
59 }
60}
61
62pub(crate) fn write_frontend_string(value: Option<String>, out: *mut c_char, len: usize) -> bool {
63 let Some(value) = value else {
64 return false;
65 };
66 let Some(buffer_len_without_nul) = len.checked_sub(1) else {
67 return false;
68 };
69 if out.is_null() {
70 return false;
71 }
72
73 let value = crate::sanitize_cstring(value);
74 copy_cstring_prefix(&value, out, buffer_len_without_nul);
75 true
76}
77
78fn copy_cstring_prefix(value: &CString, out: *mut c_char, max_bytes: usize) {
79 let bytes = value.as_bytes();
80 let copied = bytes.len().min(max_bytes);
81 unsafe {
84 std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), out, copied);
85 *out.add(copied) = 0;
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::{DiskControlInterfaceVersion, DiskIndex, DiskTrayState, write_frontend_string};
92 use std::ffi::CStr;
93
94 #[test]
95 fn disk_newtypes_preserve_raw_values() {
96 assert_eq!(DiskIndex::new(3).as_raw(), 3);
97 assert_eq!(DiskControlInterfaceVersion::new(0).get(), 0);
98 assert!(!DiskControlInterfaceVersion::new(0).supports_extended());
99 assert!(DiskControlInterfaceVersion::new(1).supports_extended());
100 }
101
102 #[test]
103 fn disk_tray_state_hides_raw_ejected_bool() {
104 assert_eq!(DiskTrayState::from_ejected(false), DiskTrayState::Closed);
105 assert_eq!(DiskTrayState::from_ejected(true), DiskTrayState::Ejected);
106 assert!(!DiskTrayState::Closed.is_ejected());
107 assert!(DiskTrayState::Ejected.is_ejected());
108 }
109
110 #[test]
111 fn frontend_string_copy_is_nul_terminated_and_sanitized() {
112 let mut out = [0i8; 8];
113
114 assert!(write_frontend_string(
115 Some("disk\0one.cue".to_string()),
116 out.as_mut_ptr(),
117 out.len()
118 ));
119
120 assert_eq!(unsafe { CStr::from_ptr(out.as_ptr()) }, c"diskone");
121 }
122}