1use hadris_fixed::FixedBytes;
2
3#[cfg(feature = "alloc")]
4use crate::joliet::JolietLevel;
5
6#[cfg(feature = "write")]
7use crate::types::{Charset, CharsetD, CharsetD1};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum EntryType {
12 Level1 {
14 supports_lowercase: bool,
16 supports_rrip: bool,
18 },
19 Level2 {
21 supports_lowercase: bool,
23 supports_rrip: bool,
25 },
26 Level3 {
28 supports_lowercase: bool,
30 supports_rrip: bool,
32 },
33 #[cfg(feature = "alloc")]
34 Joliet {
36 level: JolietLevel,
38 supports_rrip: bool,
40 },
41}
42
43impl Default for EntryType {
44 fn default() -> Self {
45 Self::Level1 {
46 supports_lowercase: false,
47 supports_rrip: false,
48 }
49 }
50}
51
52impl EntryType {
53 pub fn supports_rrip(&self) -> bool {
58 match self {
59 Self::Level1 { supports_rrip, .. } => *supports_rrip,
60 Self::Level2 { supports_rrip, .. } => *supports_rrip,
61 Self::Level3 { supports_rrip, .. } => *supports_rrip,
62 #[cfg(feature = "alloc")]
63 Self::Joliet { supports_rrip, .. } => *supports_rrip,
64 }
65 }
66
67 fn usefulness(self) -> u8 {
71 match self {
72 Self::Level1 {
73 supports_lowercase,
74 supports_rrip,
75 } => (supports_lowercase as u8) << 2 | (supports_rrip as u8) << 4,
76 Self::Level2 {
77 supports_lowercase,
78 supports_rrip,
79 } => 0x01 | (supports_lowercase as u8) << 2 | (supports_rrip as u8) << 4,
80 Self::Level3 {
81 supports_lowercase,
82 supports_rrip,
83 } => 0x02 | (supports_lowercase as u8) << 2 | (supports_rrip as u8) << 4,
84 #[cfg(feature = "alloc")]
85 Self::Joliet {
86 level,
87 supports_rrip,
88 } => (level as u8 + 11) | (supports_rrip as u8) << 4,
89 }
90 }
91}
92
93impl Ord for EntryType {
94 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
95 self.usefulness().cmp(&other.usefulness())
96 }
97}
98
99impl PartialOrd for EntryType {
100 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
101 Some(self.cmp(other))
102 }
103}
104
105#[cfg(feature = "alloc")]
106impl From<JolietLevel> for EntryType {
107 fn from(value: JolietLevel) -> Self {
108 Self::Joliet {
109 level: value,
110 supports_rrip: false,
111 }
112 }
113}
114
115pub type FilenameL1 = FixedBytes<14>;
117pub type FilenameL2 = FixedBytes<32>;
119pub type FilenameL3 = FixedBytes<207>;
121
122#[cfg(feature = "write")]
123pub enum ConvertedName {
125 Level1(FilenameL1),
127 Level2(FilenameL2),
129 Level3(FilenameL3),
131 Joliet(FixedBytes<207>),
133}
134
135#[cfg(feature = "write")]
136impl ConvertedName {
137 pub fn as_bytes(&self) -> &[u8] {
139 match self {
140 Self::Level1(name) => name.as_bytes(),
141 Self::Level2(name) => name.as_bytes(),
142 Self::Level3(name) => name.as_bytes(),
143 Self::Joliet(name) => name.as_bytes(),
144 }
145 }
146}
147
148#[cfg(feature = "write")]
149impl EntryType {
150 pub fn convert_name(self, name: &str) -> ConvertedName {
152 match self {
153 Self::Level1 {
154 supports_lowercase, ..
155 } => ConvertedName::Level1(convert_l1(name, supports_lowercase)),
156 Self::Level2 {
157 supports_lowercase, ..
158 } => ConvertedName::Level2(convert_l2(name, supports_lowercase)),
159 Self::Level3 {
160 supports_lowercase, ..
161 } => ConvertedName::Level3(convert_l3(name, supports_lowercase)),
162 Self::Joliet { level, .. } => match level {
163 JolietLevel::Level1 | JolietLevel::Level2 | JolietLevel::Level3 => {
165 ConvertedName::Joliet(convert_joliet3(name))
166 }
167 },
168 }
169 }
170
171 pub fn convert_directory_name(self, name: &str) -> ConvertedName {
173 fn primary<const N: usize>(
174 name: &str,
175 max: usize,
176 supports_lowercase: bool,
177 ) -> FixedBytes<N> {
178 let mut converted = FixedBytes::empty();
179 let end = name
180 .char_indices()
181 .map(|(offset, _)| offset)
182 .take_while(|offset| *offset <= max)
183 .last()
184 .unwrap_or(0);
185 let end = if name.len() <= max { name.len() } else { end };
186 let range = converted.push_slice(&name.as_bytes()[..end]);
187 let bytes = converted.as_bytes_mut()[range].iter_mut();
188 if supports_lowercase {
189 CharsetD1::substitute_invalid(bytes);
190 } else {
191 CharsetD::substitute_invalid(bytes);
192 }
193 converted
194 }
195
196 match self {
197 Self::Level1 {
198 supports_lowercase, ..
199 } => ConvertedName::Level1(primary(name, 8, supports_lowercase)),
200 Self::Level2 {
201 supports_lowercase, ..
202 } => ConvertedName::Level2(primary(name, 31, supports_lowercase)),
203 Self::Level3 {
204 supports_lowercase, ..
205 } => ConvertedName::Level3(primary(name, 31, supports_lowercase)),
206 Self::Joliet { .. } => ConvertedName::Joliet(convert_joliet3(name)),
207 }
208 }
209}
210
211#[cfg(feature = "write")]
212pub fn convert_l1(name: &str, supports_lowercase: bool) -> FixedBytes<14> {
214 let mut l1 = FixedBytes::empty();
215 let name_bytes = name.as_bytes();
216 match name.find('.') {
217 Some(index) => {
218 let basename = l1.push_slice(&name_bytes[0..index.min(8)]);
220 let basename = l1.as_bytes_mut()[basename].iter_mut();
221 if supports_lowercase {
222 CharsetD1::substitute_invalid(basename);
223 } else {
224 CharsetD::substitute_invalid(basename);
225 }
226 let ext_len = (name.len() - index - 1).min(3);
228 l1.push_byte(b'.');
229 let ext = l1.push_slice(&name_bytes[index + 1..(index + 1 + ext_len).min(name.len())]);
230 let ext = l1.as_bytes_mut()[ext].iter_mut();
231 if supports_lowercase {
232 CharsetD1::substitute_invalid(ext);
233 } else {
234 CharsetD::substitute_invalid(ext);
235 }
236 }
237 None => {
238 let len = name.len().min(8);
239 let basename = l1.push_slice(&name_bytes[0..len]);
240 let basename = l1.as_bytes_mut()[basename].iter_mut();
241 if supports_lowercase {
242 CharsetD1::substitute_invalid(basename);
243 } else {
244 CharsetD::substitute_invalid(basename);
245 }
246 }
247 }
248 l1.push_slice(b";1");
249 l1
250}
251
252#[cfg(feature = "write")]
253pub fn convert_l2(name: &str, supports_lowercase: bool) -> FilenameL2 {
255 let mut l2 = FilenameL2::empty();
256 let name_bytes = name.as_bytes();
257 const MAX_NAME_LEN: usize = 30;
259
260 match name.find('.') {
261 Some(index) => {
262 let basename_end = index.min(MAX_NAME_LEN);
263 let basename = l2.push_slice(&name_bytes[0..basename_end]);
264 let basename = l2.as_bytes_mut()[basename].iter_mut();
265 if supports_lowercase {
266 CharsetD1::substitute_invalid(basename);
267 } else {
268 CharsetD::substitute_invalid(basename);
269 }
270
271 let remaining = MAX_NAME_LEN.saturating_sub(basename_end + 1);
273 if remaining > 0 {
274 l2.push_byte(b'.');
275 let ext_end = (index + 1 + remaining).min(name.len());
276 let ext = l2.push_slice(&name_bytes[index + 1..ext_end]);
277 let ext = l2.as_bytes_mut()[ext].iter_mut();
278 if supports_lowercase {
279 CharsetD1::substitute_invalid(ext);
280 } else {
281 CharsetD::substitute_invalid(ext);
282 }
283 }
284 }
285 None => {
286 let len = name.len().min(MAX_NAME_LEN);
287 let basename = l2.push_slice(&name_bytes[0..len]);
288 let basename = l2.as_bytes_mut()[basename].iter_mut();
289 if supports_lowercase {
290 CharsetD1::substitute_invalid(basename);
291 } else {
292 CharsetD::substitute_invalid(basename);
293 }
294 }
295 }
296 l2.push_slice(b";1");
297 l2
298}
299
300#[cfg(feature = "write")]
301pub fn convert_l3(name: &str, supports_lowercase: bool) -> FilenameL3 {
303 let mut l3 = FilenameL3::empty();
304 let name_bytes = name.as_bytes();
305 const MAX_NAME_LEN: usize = 207;
307
308 match name.find('.') {
309 Some(index) => {
310 let basename_end = index.min(MAX_NAME_LEN);
311 let basename = l3.push_slice(&name_bytes[0..basename_end]);
312 let basename = l3.as_bytes_mut()[basename].iter_mut();
313 if supports_lowercase {
314 CharsetD1::substitute_invalid(basename);
315 } else {
316 CharsetD::substitute_invalid(basename);
317 }
318
319 let remaining = MAX_NAME_LEN.saturating_sub(basename_end + 1);
321 if remaining > 0 {
322 l3.push_byte(b'.');
323 let ext_end = (index + 1 + remaining).min(name.len());
324 let ext = l3.push_slice(&name_bytes[index + 1..ext_end]);
325 let ext = l3.as_bytes_mut()[ext].iter_mut();
326 if supports_lowercase {
327 CharsetD1::substitute_invalid(ext);
328 } else {
329 CharsetD::substitute_invalid(ext);
330 }
331 }
332 }
333 None => {
334 let len = name.len().min(MAX_NAME_LEN);
335 let basename = l3.push_slice(&name_bytes[0..len]);
336 let basename = l3.as_bytes_mut()[basename].iter_mut();
337 if supports_lowercase {
338 CharsetD1::substitute_invalid(basename);
339 } else {
340 CharsetD::substitute_invalid(basename);
341 }
342 }
343 }
344 l3
345}
346
347#[cfg(feature = "write")]
353pub const JOLIET_MAX_NAME_CHARS: usize = 64;
354
355#[cfg(feature = "write")]
356pub fn convert_joliet3(name: &str) -> FixedBytes<207> {
364 let mut j1 = FixedBytes::empty();
365 for c in name.chars().take(JOLIET_MAX_NAME_CHARS) {
366 let unit = if (c as u32) <= 0xFFFF {
367 c as u16
368 } else {
369 b'_' as u16
370 };
371 j1.push_slice(&unit.to_be_bytes());
372 }
373
374 j1
375}
376
377#[cfg(all(test, feature = "write", feature = "alloc"))]
378mod joliet_tests {
379 use super::*;
380 use crate::joliet::decode_joliet_name;
381
382 #[test]
383 fn joliet_name_is_truncated_to_the_conformant_limit() {
384 let long = "a".repeat(200);
385 let encoded = convert_joliet3(&long);
386 assert_eq!(encoded.as_bytes().len(), JOLIET_MAX_NAME_CHARS * 2);
388 assert_eq!(decode_joliet_name(encoded.as_bytes()).chars().count(), 64);
389 }
390
391 #[test]
392 fn joliet_substitutes_non_bmp_instead_of_emitting_surrogates() {
393 let encoded = convert_joliet3("a\u{1F600}b.txt");
395 let decoded = decode_joliet_name(encoded.as_bytes());
396 assert_eq!(decoded, "a_b.txt");
397 for pair in encoded.as_bytes().chunks_exact(2) {
399 let unit = u16::from_be_bytes([pair[0], pair[1]]);
400 assert!(
401 !(0xD800..=0xDFFF).contains(&unit),
402 "surrogate leaked into UCS-2"
403 );
404 }
405 }
406
407 #[test]
408 fn joliet_preserves_bmp_unicode() {
409 let encoded = convert_joliet3("日本語.txt");
410 assert_eq!(decode_joliet_name(encoded.as_bytes()), "日本語.txt");
411 }
412}