1pub static ESCAPE_SEQUNCES: [[u8; 3]; 3] = [*b"%/@", *b"%/C", *b"%/E"];
7
8#[repr(u8)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum JolietLevel {
12 Level1 = 1,
14 Level2 = 2,
16 Level3 = 3,
18}
19
20impl JolietLevel {
21 pub fn all() -> &'static [JolietLevel] {
23 static LEVELS: [JolietLevel; 3] = [
24 JolietLevel::Level1,
25 JolietLevel::Level2,
26 JolietLevel::Level3,
27 ];
28 &LEVELS
29 }
30
31 pub fn escape_sequence(self) -> [u8; 32] {
33 let mut output = [b' '; 32];
34 match self {
35 Self::Level1 => output[0..3].copy_from_slice(b"%/@"),
36 Self::Level2 => output[0..3].copy_from_slice(b"%/C"),
37 Self::Level3 => output[0..3].copy_from_slice(b"%/E"),
38 }
39 output
40 }
41
42 pub fn from_escape_sequence(escape: &[u8; 32]) -> Option<Self> {
44 if escape[0..3] == *b"%/@" {
45 Some(Self::Level1)
46 } else if escape[0..3] == *b"%/C" {
47 Some(Self::Level2)
48 } else if escape[0..3] == *b"%/E" {
49 Some(Self::Level3)
50 } else {
51 None
52 }
53 }
54}
55
56#[cfg(feature = "alloc")]
61pub fn decode_joliet_name(bytes: &[u8]) -> alloc::string::String {
62 if bytes.len() < 2 {
64 return alloc::string::String::new();
65 }
66
67 let bytes = strip_version_suffix(bytes);
69
70 let code_units: alloc::vec::Vec<u16> = bytes
72 .chunks_exact(2)
73 .map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
74 .collect();
75
76 alloc::string::String::from_utf16_lossy(&code_units)
78}
79
80#[cfg(feature = "alloc")]
82fn strip_version_suffix(bytes: &[u8]) -> &[u8] {
83 if bytes.len() >= 4 {
85 let suffix = &bytes[bytes.len() - 4..];
86 if suffix == [0x00, b';', 0x00, b'1'] {
87 return &bytes[..bytes.len() - 4];
88 }
89 }
90 bytes
91}
92
93#[cfg(feature = "alloc")]
99pub fn encode_joliet_name(name: &str) -> alloc::vec::Vec<u8> {
100 let mut result = alloc::vec::Vec::with_capacity(name.chars().count() * 2);
101 for c in name.chars() {
102 let unit = if (c as u32) <= 0xFFFF {
103 c as u16
104 } else {
105 b'_' as u16
106 };
107 result.extend_from_slice(&unit.to_be_bytes());
108 }
109 result
110}
111
112pub fn is_likely_joliet_name(bytes: &[u8]) -> bool {
116 if !bytes.len().is_multiple_of(2) || bytes.is_empty() {
118 return false;
119 }
120
121 let mut ascii_count = 0;
124 for pair in bytes.chunks_exact(2) {
125 if pair[0] == 0x00 && pair[1].is_ascii_graphic() {
126 ascii_count += 1;
127 }
128 }
129
130 ascii_count * 2 > bytes.len() / 2
132}
133
134#[cfg(all(feature = "std", test))]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_escape_sequences() {
140 let level1 = b"%/@";
141 assert_eq!(level1, &ESCAPE_SEQUNCES[0]);
142
143 let level2 = b"%/C";
144 assert_eq!(level2, &ESCAPE_SEQUNCES[1]);
145
146 let level3 = b"%/E";
147 assert_eq!(level3, &ESCAPE_SEQUNCES[2]);
148 }
149
150 #[test]
151 fn test_joliet_level_escape_sequence() {
152 let level1 = JolietLevel::Level1.escape_sequence();
153 assert_eq!(&level1[0..3], b"%/@");
154
155 let level2 = JolietLevel::Level2.escape_sequence();
156 assert_eq!(&level2[0..3], b"%/C");
157
158 let level3 = JolietLevel::Level3.escape_sequence();
159 assert_eq!(&level3[0..3], b"%/E");
160 }
161
162 #[test]
163 fn test_joliet_level_from_escape_sequence() {
164 let mut seq = [b' '; 32];
165
166 seq[0..3].copy_from_slice(b"%/@");
167 assert_eq!(
168 JolietLevel::from_escape_sequence(&seq),
169 Some(JolietLevel::Level1)
170 );
171
172 seq[0..3].copy_from_slice(b"%/C");
173 assert_eq!(
174 JolietLevel::from_escape_sequence(&seq),
175 Some(JolietLevel::Level2)
176 );
177
178 seq[0..3].copy_from_slice(b"%/E");
179 assert_eq!(
180 JolietLevel::from_escape_sequence(&seq),
181 Some(JolietLevel::Level3)
182 );
183
184 seq[0..3].copy_from_slice(b"XXX");
185 assert_eq!(JolietLevel::from_escape_sequence(&seq), None);
186 }
187
188 #[test]
189 fn test_joliet_level_all() {
190 let levels = JolietLevel::all();
191 assert_eq!(levels.len(), 3);
192 assert_eq!(levels[0], JolietLevel::Level1);
193 assert_eq!(levels[1], JolietLevel::Level2);
194 assert_eq!(levels[2], JolietLevel::Level3);
195 }
196
197 #[test]
198 fn test_encode_joliet_name_ascii() {
199 let encoded = encode_joliet_name("test.txt");
200 assert_eq!(encoded.len(), 16); assert_eq!(&encoded[0..2], &[0x00, b't']);
203 assert_eq!(&encoded[2..4], &[0x00, b'e']);
204 assert_eq!(&encoded[4..6], &[0x00, b's']);
205 assert_eq!(&encoded[6..8], &[0x00, b't']);
206 }
207
208 #[test]
209 fn test_encode_joliet_name_unicode() {
210 let encoded = encode_joliet_name("日本語");
211 assert_eq!(encoded.len(), 6); assert_eq!(&encoded[0..2], &[0x65, 0xE5]);
216 }
217
218 #[test]
219 fn test_decode_joliet_name_ascii() {
220 let bytes: &[u8] = &[0x00, b't', 0x00, b'e', 0x00, b's', 0x00, b't'];
222 let decoded = decode_joliet_name(bytes);
223 assert_eq!(decoded, "test");
224 }
225
226 #[test]
227 fn test_decode_joliet_name_unicode() {
228 let bytes: &[u8] = &[0x65, 0xE5, 0x67, 0x2C];
230 let decoded = decode_joliet_name(bytes);
231 assert_eq!(decoded, "日本");
232 }
233
234 #[test]
235 fn test_decode_joliet_name_with_version_suffix() {
236 let bytes: &[u8] = &[
238 0x00, b't', 0x00, b'e', 0x00, b's', 0x00, b't', 0x00, b';', 0x00, b'1',
239 ];
240 let decoded = decode_joliet_name(bytes);
241 assert_eq!(decoded, "test"); }
243
244 #[test]
245 fn test_decode_joliet_name_empty() {
246 let decoded = decode_joliet_name(&[]);
247 assert_eq!(decoded, "");
248
249 let decoded = decode_joliet_name(&[0x00]); assert_eq!(decoded, "");
251 }
252
253 #[test]
254 fn test_encode_decode_roundtrip() {
255 let original = "test_file.txt";
256 let encoded = encode_joliet_name(original);
257 let decoded = decode_joliet_name(&encoded);
258 assert_eq!(decoded, original);
259 }
260
261 #[test]
262 fn test_encode_decode_roundtrip_unicode() {
263 let original = "文档_2024.txt";
264 let encoded = encode_joliet_name(original);
265 let decoded = decode_joliet_name(&encoded);
266 assert_eq!(decoded, original);
267 }
268
269 #[test]
270 fn test_is_likely_joliet_name_ascii() {
271 let joliet: &[u8] = &[0x00, b't', 0x00, b'e', 0x00, b's', 0x00, b't'];
273 assert!(is_likely_joliet_name(joliet));
274 }
275
276 #[test]
277 fn test_is_likely_joliet_name_odd_length() {
278 let bytes: &[u8] = &[0x00, b't', 0x00];
280 assert!(!is_likely_joliet_name(bytes));
281 }
282
283 #[test]
284 fn test_is_likely_joliet_name_empty() {
285 assert!(!is_likely_joliet_name(&[]));
286 }
287
288 #[test]
289 fn test_is_likely_joliet_name_non_ascii() {
290 let iso_name: &[u8] = b"TEST.TXT";
292 assert!(!is_likely_joliet_name(iso_name));
293 }
294}