Skip to main content

libretro_core/
disk.rs

1//! Disk-control newtypes and retained string helpers.
2//!
3//! The public `Core` disk methods use `DiskIndex`, `DiskTrayState`, and
4//! `DiskControlInterfaceVersion` so media state does not leak raw libretro
5//! integer conventions into core logic.
6
7use 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    // SAFETY: The caller provides a non-null output buffer with room for
82    // `max_bytes + 1` bytes. We copy at most that many bytes and always append NUL.
83    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}