cdtext/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5use alloc::{borrow::ToOwned, string::String, vec::Vec};
6use num_derive::FromPrimitive;
7use num_traits::FromPrimitive;
8
9/// Main parser structure.
10pub struct CDText<'data> {
11    _length: usize,
12    data: &'data [u8],
13}
14
15/// The pack type
16#[derive(Debug, FromPrimitive, PartialEq, Clone, Copy)]
17pub enum CDTextPackType {
18    Title = 0x80,
19    Performers = 0x81,
20    Songwriters = 0x82,
21    Composers = 0x83,
22    Arrangers = 0x84,
23    Message = 0x85,
24    DiscID = 0x86,
25    Genre = 0x87,
26    TOC = 0x88,
27    AdditionalTOC = 0x89,
28    ClosedInfo = 0x8d,
29    Code = 0x8e,
30    BlockSizeInfo = 0x8f,
31}
32
33/// Track number entry referring to.
34/// Entry can refer to whole album or on separate track in it.
35#[derive(Debug, PartialEq, Clone, Copy)]
36pub enum CDTextTrackNumber {
37    WholeAlbum,
38    Track(u8),
39}
40
41/// A pack itself.
42#[derive(Debug, Clone)]
43pub struct CDTextPack {
44    pub pack_type: CDTextPackType,
45    pub track_number: CDTextTrackNumber,
46    pub seq_counter: u8,
47    pub character_position: u8,
48    pub block_number: u8,
49    pub is_double_byte_characters: bool,
50
51    pub payload: [u8; 12],
52    pub crc: u16,
53}
54
55/// Data can be represented as string or raw data.
56#[derive(Debug, Clone)]
57pub enum CDTextEntryDataType {
58    String(String),
59    Data(Vec<u8>),
60}
61
62/// The processed entry.
63#[derive(Debug, Clone)]
64pub struct CDTextEntry {
65    pub track_number: CDTextTrackNumber,
66    pub entry_type: CDTextPackType,
67    pub data: CDTextEntryDataType,
68}
69
70impl<'data> CDText<'data> {
71    /// Creates a parser from data, assuming that first 4 bytes are used for service info.
72    /// First two bytes are the data length minus two.
73    pub fn from_data_with_length(data: &'data [u8]) -> Self {
74        Self {
75            _length: (((data[0] as usize) << 8) | (data[1] as usize)) - 2,
76            data: &data[4..],
77        }
78    }
79
80    /// Creates a parser from data.
81    pub fn from_data(data: &'data [u8]) -> Self {
82        Self {
83            _length: data.len(),
84            data,
85        }
86    }
87
88    /// Internal method. Parses a separate pack from data.
89    /// Data (sub)slice must be 18 bytes long.
90    fn parse_pack(&self, subdata: &[u8]) -> Option<CDTextPack> {
91        debug_assert!(subdata.len() == 18);
92
93        // The first byte of each pack contains the pack type.
94        let pack_type = CDTextPackType::from_u8(subdata[0])?;
95
96        // The second byte often gives the track number of the pack.
97        let track_number = match subdata[1] {
98            // However, a zero track value indicates that the information pertains to the whole album.
99            0 => CDTextTrackNumber::WholeAlbum,
100            n => CDTextTrackNumber::Track(n),
101        };
102
103        // The third byte is a sequential counter.
104        let seq_counter = subdata[2];
105
106        // bits 0-3: Character position.
107        let character_position = subdata[3] & 0b1111;
108
109        // bits 4-6: Block number
110        let block_nr = (subdata[3] >> 4) & 0b111;
111
112        // bit 7: Is 0 if single byte characters, 1 if double-byte characters.
113        let is_double_byte_chars = ((subdata[3] >> 7) & 1) != 0;
114
115        let payload = &subdata[4..16];
116
117        let crc = u16::from_be_bytes(subdata[16..18].try_into().unwrap());
118
119        Some(CDTextPack {
120            pack_type,
121            track_number,
122            seq_counter,
123            character_position,
124            block_number: block_nr,
125            is_double_byte_characters: is_double_byte_chars,
126            payload: payload.try_into().unwrap(),
127            crc,
128        })
129    }
130
131    /// Wrapper method.
132    pub fn iter_pack_chunks(&self) -> impl Iterator<Item = Option<CDTextPack>> {
133        // Each pack consists of a 4-byte header, 12 bytes of payload, and 2 bytes of CRC.
134        // 4 + 12 + 2 = 18
135        self.data.chunks(18).map(|x| self.parse_pack(x))
136    }
137
138    /// Parses all the entries from the data and returns a Vec with parsed entries.
139    pub fn parse(&self) -> Vec<CDTextEntry> {
140        let mut payload_buffer: Vec<u8> = Vec::with_capacity(16);
141        let mut prev_pack = self.iter_pack_chunks().next().unwrap().unwrap();
142
143        let mut parsed_data: Vec<CDTextEntry> = Vec::new();
144
145        for pack in self.iter_pack_chunks().skip(1) {
146            let pack = pack.as_ref().unwrap();
147
148            // let index = if pack.character_position <= 12 {
149            //     12 - pack.character_position
150            // } else {
151            //     0
152            // } as usize;
153
154            let index = 12u8.saturating_sub(pack.character_position) as usize;
155
156            match pack.pack_type {
157                CDTextPackType::Arrangers
158                | CDTextPackType::Composers
159                | CDTextPackType::Title
160                | CDTextPackType::Performers
161                | CDTextPackType::Songwriters => {
162                    let mut track_number = prev_pack.track_number;
163                    let mut before = &prev_pack.payload[..index];
164                    let after = &prev_pack.payload[index..];
165
166                    let is_terminal = before.ends_with(&[0]);
167
168                    // I don't know why 2.
169                    // More than one nul-terminated strings can be encountered in one entry (usually in short strings).
170                    // So we need to handle it somehow.
171                    if before.iter().filter(|&x| *x == 0).count() == 2 {
172                        // println!("===== INCREMENT! {before:?}");
173
174                        let position = before.iter().position(|&x| x == 0).unwrap();
175                        payload_buffer.extend_from_slice(&before[..position]);
176
177                        if !payload_buffer.is_empty() {
178                            // println!("===== PAYLOAD: {payload_buffer:?}");
179
180                            parsed_data.push(CDTextEntry {
181                                track_number,
182                                entry_type: prev_pack.pack_type,
183                                data: CDTextEntryDataType::String(
184                                    str::from_utf8(&payload_buffer).unwrap().to_owned(),
185                                ),
186                            });
187                        } else {
188                            parsed_data.push(CDTextEntry {
189                                track_number,
190                                entry_type: prev_pack.pack_type,
191                                data: CDTextEntryDataType::String(
192                                    str::from_utf8(&payload_buffer).unwrap().to_owned(),
193                                ),
194                            });
195                        }
196
197                        payload_buffer.clear();
198
199                        before = &before[before.iter().position(|&x| x == 0).unwrap_or(0) + 1..];
200
201                        if let CDTextTrackNumber::Track(nr) = track_number {
202                            track_number = CDTextTrackNumber::Track(nr + 1);
203                        } else if let CDTextTrackNumber::WholeAlbum = track_number {
204                            track_number = CDTextTrackNumber::Track(1);
205                        }
206                    }
207
208                    payload_buffer.extend_from_slice(if is_terminal {
209                        let len = before.iter().rev().position(|x| *x != 0);
210
211                        if let Some(ix) = len {
212                            &before[..before.len() - ix]
213                        } else {
214                            before
215                        }
216                    } else {
217                        before
218                    });
219
220                    // println!("Before: {before:?}");
221                    // println!("After: {after:?}");
222
223                    // println!("{:x?} ({:?} / {index})", pack, unsafe {
224                    //     str::from_utf8_unchecked(&payload_buffer)
225                    // });
226
227                    if is_terminal {
228                        parsed_data.push(CDTextEntry {
229                            track_number,
230                            entry_type: prev_pack.pack_type,
231                            data: CDTextEntryDataType::String(
232                                str::from_utf8(&payload_buffer).unwrap().trim_end_matches(|x| x as u32 == 0).to_owned(),
233                            ),
234                        });
235
236                        payload_buffer.clear();
237                    }
238
239                    payload_buffer.extend_from_slice(after);
240                }
241                _ => {
242                    break;
243                },
244            };
245
246            prev_pack = pack.clone();
247        }
248
249        // println!("[{payload_buffer:?}]: Prev pack: {prev_pack:?}");
250
251        payload_buffer.extend_from_slice(&prev_pack.payload[..prev_pack.payload.iter().position(|&x| x == 0).unwrap()]);
252
253        parsed_data.push(CDTextEntry {
254            track_number: prev_pack.track_number,
255            entry_type: prev_pack.pack_type,
256            data: CDTextEntryDataType::String(
257                str::from_utf8(&payload_buffer).unwrap().to_owned(),
258            ),
259        });
260
261        // println!("Length is: {}", self.length);
262
263        // for i in parsed_data {
264        //     println!("{:?} => {:?}", i.track_number, i.data);
265        // }
266
267        parsed_data
268    }
269}