Skip to main content

classicube_sys/
string.rs

1use core::{
2    borrow::Borrow,
3    ffi::CStr,
4    fmt::{self, Display},
5    slice,
6};
7
8use crate::{
9    String_CalcLen,
10    bindings::{STRING_SIZE, cc_codepoint, cc_string, cc_uint16, cc_unichar},
11    std_types::{Box, CString, String, ToString, Vec, c_char, c_int},
12};
13
14impl cc_string {
15    #[must_use]
16    pub fn as_slice(&self) -> &[u8] {
17        let len = self.length as usize;
18        let data = self.buffer as *const u8;
19        if len == 0 || data.is_null() {
20            return &[];
21        }
22        unsafe { slice::from_raw_parts(data, len) }
23    }
24}
25
26impl Display for cc_string {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        String::from_utf16_lossy(
29            &self
30                .as_slice()
31                .iter()
32                .map(|c| Convert_CP437ToUnicode(*c))
33                .collect::<Vec<_>>(),
34        )
35        .fmt(f)
36    }
37}
38
39impl From<cc_string> for String {
40    fn from(cc_string: cc_string) -> Self {
41        cc_string.to_string()
42    }
43}
44
45pub struct OwnedString {
46    cc_string: cc_string,
47
48    #[allow(dead_code)]
49    c_str: Box<CStr>,
50}
51
52impl OwnedString {
53    /// # Panics
54    ///
55    /// Panics if the CP437-encoded bytes contain an interior NUL — only
56    /// possible when the input string contains a U+0000 character, since
57    /// `Convert_CodepointToCP437` maps unrepresentable codepoints to `'?'`.
58    /// Also panics if the encoded length exceeds [`cc_uint16::MAX`].
59    pub fn new<S: Into<String>>(s: S) -> Self {
60        let bytes = s
61            .into()
62            // TODO is chars() "codepoints" in cc's definition?
63            .chars()
64            .map(|c| c as cc_codepoint)
65            .map(Convert_CodepointToCP437)
66            .collect::<Vec<_>>();
67        let length = bytes.len();
68        let capacity = bytes.len();
69
70        let c_str = CString::new(bytes).unwrap().into_boxed_c_str();
71        let buffer: *const c_char = c_str.as_ptr();
72
73        let length = cc_uint16::try_from(length).expect("string length exceeds cc_string capacity");
74        let capacity =
75            cc_uint16::try_from(capacity).expect("string length exceeds cc_string capacity");
76
77        Self {
78            c_str,
79            cc_string: cc_string {
80                buffer: buffer.cast_mut(),
81                length,
82                capacity,
83            },
84        }
85    }
86
87    #[must_use]
88    pub fn as_cc_string(&self) -> &cc_string {
89        &self.cc_string
90    }
91
92    /// # Safety
93    ///
94    /// The `OwnedString` needs to live longer than the `cc_string` return here.
95    #[must_use]
96    pub unsafe fn get_cc_string(&self) -> cc_string {
97        cc_string { ..self.cc_string }
98    }
99}
100
101impl Borrow<cc_string> for OwnedString {
102    fn borrow(&self) -> &cc_string {
103        self.as_cc_string()
104    }
105}
106
107#[test]
108fn test_owned_string() {
109    fn use_cc_string<T: Borrow<cc_string>>(s: T) {
110        #[cfg(not(feature = "no_std"))]
111        {
112            println!("{:?}", s.borrow());
113        }
114    }
115
116    let owned_string = OwnedString::new("hello");
117
118    use_cc_string(owned_string.as_cc_string());
119
120    use_cc_string(owned_string);
121
122    // let s: cc_string = owned_string.into();
123}
124
125/// Constructs a string from the given arguments.
126///
127/// # Safety
128///
129/// The `buffer` needs to live longer than the `cc_string`.
130///
131/// # Panics
132///
133/// Panics if `length` or `capacity` does not fit in [`cc_uint16`].
134pub unsafe fn String_Init(buffer: *mut c_char, length: c_int, capacity: c_int) -> cc_string {
135    cc_string {
136        buffer,
137        length: cc_uint16::try_from(length).expect("cc_string length out of range"),
138        capacity: cc_uint16::try_from(capacity).expect("cc_string capacity out of range"),
139    }
140}
141
142/// Constructs a string from a (maybe null terminated) buffer.
143///
144/// # Safety
145///
146/// The `buffer` needs to live longer than the `cc_string`.
147#[must_use]
148pub unsafe fn String_FromRaw(buffer: *mut c_char, capacity: c_int) -> cc_string {
149    unsafe { String_Init(buffer, String_CalcLen(buffer, capacity), capacity) }
150}
151
152/// Constructs a string from a compile time array, that may have arbitary actual length of data at runtime
153///
154/// # Safety
155///
156/// The `buffer` needs to live longer than the `cc_string`.
157///
158/// # Panics
159///
160/// The function will panic if `buffer.len()` is outside the range of `c_int`.
161#[must_use]
162pub unsafe fn String_FromRawArray(buffer: &mut [c_char]) -> cc_string {
163    unsafe { String_FromRaw(buffer.as_mut_ptr(), c_int::try_from(buffer.len()).unwrap()) }
164}
165
166/// # Safety
167///
168/// `data` must have at least `STRING_SIZE` bytes and outlive the returned
169/// `cc_string`, which borrows it as a non-owning buffer.
170#[must_use]
171#[expect(
172    clippy::cast_possible_wrap,
173    reason = "STRING_SIZE and resulting length fit in c_int"
174)]
175pub unsafe fn UNSAFE_GetString(data: &[u8]) -> cc_string {
176    let mut length = 0;
177    for i in (0..STRING_SIZE).rev() {
178        let code = data[i as usize];
179        if code == b'\0' || code == b' ' {
180            continue;
181        }
182        length = i + 1;
183        break;
184    }
185
186    unsafe {
187        String_Init(
188            data.as_ptr() as *mut c_char,
189            length as c_int,
190            STRING_SIZE as c_int,
191        )
192    }
193}
194
195#[test]
196fn test_get_string() {
197    unsafe {
198        let mut s =
199            b"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl0000000000000000"
200                .to_vec();
201        s.resize(STRING_SIZE as usize, 0);
202        assert_eq!(
203            UNSAFE_GetString(&s).to_string(),
204            "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
205        );
206    }
207}
208
209pub const controlChars: &[cc_unichar] = &[
210    0x0000, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, 0x25D8, 0x25CB, 0x25D9, 0x2642,
211    0x2640, 0x266A, 0x266B, 0x263C, 0x25BA, 0x25C4, 0x2195, 0x203C, 0x00B6, 0x00A7, 0x25AC, 0x21A8,
212    0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC,
213];
214
215pub const extendedChars: &[cc_unichar] = &[
216    0x2302, 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8,
217    0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB,
218    0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, 0x00E1, 0x00ED, 0x00F3,
219    0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB,
220    0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551,
221    0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E,
222    0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565,
223    0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590,
224    0x2580, 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9,
225    0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7,
226    0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0,
227];
228
229#[must_use]
230pub fn Convert_CP437ToUnicode(raw: u8) -> cc_unichar {
231    if raw < 0x20 {
232        controlChars[raw as usize]
233    } else if raw < 0x7F {
234        cc_unichar::from(raw)
235    } else {
236        extendedChars[raw as usize - 0x7F]
237    }
238}
239
240#[must_use]
241pub fn Convert_CodepointToCP437(cp: cc_codepoint) -> u8 {
242    let mut c: u8 = 0;
243    Convert_TryCodepointToCP437(cp, &mut c);
244    c
245}
246
247fn ReduceEmoji(cp: cc_codepoint) -> cc_codepoint {
248    if cp == 0x1F31E {
249        return 0x263C;
250    }
251    if cp == 0x1F3B5 {
252        return 0x266B;
253    }
254    if cp == 0x1F642 {
255        return 0x263A;
256    }
257
258    if cp == 0x1F600 || cp == 0x1F601 || cp == 0x1F603 {
259        return 0x263A;
260    }
261    if cp == 0x1F604 || cp == 0x1F606 || cp == 0x1F60A {
262        return 0x263A;
263    }
264    cp
265}
266
267#[expect(
268    clippy::cast_possible_truncation,
269    reason = "codepoint and lookup indices have been range-checked to fit in u8"
270)]
271fn Convert_TryCodepointToCP437(mut cp: cc_codepoint, c: &mut u8) -> bool {
272    if (0x20..0x7F).contains(&cp) {
273        *c = cp as u8;
274        return true;
275    }
276    if cp >= 0x1F000 {
277        cp = ReduceEmoji(cp);
278    }
279
280    for (i, &chr) in controlChars.iter().enumerate() {
281        if cc_codepoint::from(chr) == cp {
282            *c = i as u8;
283            return true;
284        }
285    }
286
287    for (i, &chr) in extendedChars.iter().enumerate() {
288        if cc_codepoint::from(chr) == cp {
289            *c = (i + 0x7F) as u8;
290            return true;
291        }
292    }
293
294    *c = b'?';
295    false
296}
297
298#[test]
299fn test_cp_437_conversion() {
300    let bytes: &[u8] = &[97, 236, 236]; // "a∞∞"
301
302    let c_str = CString::new(bytes).unwrap();
303    let len = cc_uint16::try_from(bytes.len()).unwrap();
304    let a = cc_string {
305        buffer: c_str.as_ptr().cast_mut(),
306        length: len,
307        capacity: len,
308    };
309    assert_eq!(a.to_string(), "a∞∞");
310
311    let str = "a∞∞";
312    let s = OwnedString::new(str);
313    unsafe {
314        assert_eq!(
315            slice::from_raw_parts(
316                s.as_cc_string().buffer as *const u8,
317                s.as_cc_string().length as usize
318            ),
319            bytes
320        );
321    }
322}