1use core::fmt;
2
3use hadris_fixed::FixedBytes;
4
5#[repr(transparent)]
7#[derive(Clone, Copy, PartialEq, Eq)]
8pub struct ShortFileName(FixedBytes<12>);
9
10impl fmt::Debug for ShortFileName {
11 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12 f.debug_tuple("ShortFileName")
13 .field(&self.as_str())
14 .finish()
15 }
16}
17
18#[derive(Debug)]
19pub struct CreateShortFileNameError;
21
22impl fmt::Display for CreateShortFileNameError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 f.write_str("disallowed characters in short file name")
25 }
26}
27
28#[cfg(feature = "std")]
29impl std::error::Error for CreateShortFileNameError {}
30
31impl ShortFileName {
32 pub const ALLOWED_SYMBOLS: &'static [u8] = b"$%'-_@~`!(){}^#&";
34
35 pub fn new(bytes: [u8; 11]) -> Result<Self, CreateShortFileNameError> {
37 if bytes == *b". " {
39 let mut name = FixedBytes::empty();
40 name.push_byte(b'.');
41 return Ok(Self(name));
42 }
43 if bytes == *b".. " {
44 let mut name = FixedBytes::empty();
45 name.push_slice(b"..");
46 return Ok(Self(name));
47 }
48
49 for byte in &bytes {
50 if byte.is_ascii_uppercase()
51 || Self::ALLOWED_SYMBOLS.contains(byte)
52 || byte.is_ascii_digit()
53 || *byte == b' '
54 || *byte > 127
55 {
56 continue;
57 }
58 return Err(CreateShortFileNameError);
59 }
60
61 let mut name = FixedBytes::empty();
62 name.push_slice(&bytes[0..8]);
63 name.push_byte(b'.');
64 name.push_slice(&bytes[8..11]);
65 Ok(Self(name))
66 }
67
68 pub fn raw_bytes(&self) -> [u8; 11] {
76 let bytes = self.0.as_bytes();
77 let mut result = [b' '; 11];
78 let dot_pos = bytes.iter().position(|&b| b == b'.').unwrap_or(bytes.len());
80 let name_len = dot_pos.min(8);
81 result[..name_len].copy_from_slice(&bytes[..name_len]);
82 if dot_pos < bytes.len() {
83 let ext_start = dot_pos + 1;
84 let ext_len = (bytes.len() - ext_start).min(3);
85 result[8..8 + ext_len].copy_from_slice(&bytes[ext_start..ext_start + ext_len]);
86 }
87 result
88 }
89
90 pub fn as_str(&self) -> &str {
92 self.0.as_str()
93 }
94
95 pub fn with_nt_case(&self, flags: crate::raw::NtCaseFlags) -> ShortFileName {
105 use crate::raw::NtCaseFlags;
106
107 fn push_maybe_lower(out: &mut FixedBytes<12>, part: &[u8], lower: bool) {
108 for &byte in part {
109 out.push_byte(if lower {
110 byte.to_ascii_lowercase()
111 } else {
112 byte
113 });
114 }
115 }
116
117 let bytes = self.0.as_bytes();
118 let dot = bytes.iter().position(|&b| b == b'.');
119 let base_end = dot.unwrap_or(bytes.len());
120 let mut out = FixedBytes::<12>::empty();
121 push_maybe_lower(
122 &mut out,
123 &bytes[..base_end],
124 flags.contains(NtCaseFlags::LOWER_BASE),
125 );
126 if let Some(dot) = dot {
127 out.push_byte(b'.');
128 push_maybe_lower(
129 &mut out,
130 &bytes[dot + 1..],
131 flags.contains(NtCaseFlags::LOWER_EXT),
132 );
133 }
134 ShortFileName(out)
135 }
136
137 pub fn matches(&self, name: &str) -> bool {
140 let raw = self.0.as_str();
141
142 let (our_base, our_ext) = if let Some(dot_pos) = raw.find('.') {
144 (raw[..dot_pos].trim_end(), raw[dot_pos + 1..].trim_end())
145 } else {
146 (raw.trim_end(), "")
147 };
148
149 let (search_base, search_ext) = if let Some(dot_pos) = name.rfind('.') {
151 (&name[..dot_pos], &name[dot_pos + 1..])
152 } else {
153 (name, "")
154 };
155
156 our_base.eq_ignore_ascii_case(search_base) && our_ext.eq_ignore_ascii_case(search_ext)
158 }
159
160 pub fn lfn_checksum(&self) -> u8 {
163 let name = self.raw_bytes();
164 let mut sum: u8 = 0;
165 for &byte in &name {
166 sum = sum.rotate_right(1).wrapping_add(byte);
168 }
169 sum
170 }
171
172 #[cfg(feature = "write")]
174 pub fn to_raw_bytes(&self) -> [u8; 11] {
175 self.raw_bytes()
176 }
177
178 #[cfg(feature = "write")]
191 pub fn from_long_name(name: &str, suffix: u8) -> Result<Self, CreateShortFileNameError> {
192 Self::from_long_name_with(name, suffix, &crate::oem::LossyAsciiOemCpConverter)
193 }
194
195 #[cfg(feature = "write")]
201 pub fn from_long_name_with(
202 name: &str,
203 suffix: u8,
204 oem: &dyn crate::oem::OemCpConverter,
205 ) -> Result<Self, CreateShortFileNameError> {
206 let (base, ext) = match name.rfind('.') {
208 Some(pos) if pos > 0 => (&name[..pos], &name[pos + 1..]),
209 _ => (name, ""),
210 };
211
212 let mut base_chars = [b' '; 8];
214 let mut base_len = 0;
215 for ch in base.chars() {
216 if base_len >= 6 && suffix > 0 {
217 break;
219 }
220 if base_len >= 8 {
221 break;
222 }
223 let processed = Self::process_char(ch, oem);
224 if processed != 0 {
225 base_chars[base_len] = processed;
226 base_len += 1;
227 }
228 }
229
230 if suffix > 0 {
232 if suffix <= 4 {
233 let max_base = 6; if base_len > max_base {
236 base_len = max_base;
237 }
238 base_chars[base_len] = b'~';
239 base_len += 1;
240 base_chars[base_len] = b'0' + suffix;
241 base_len += 1;
242 } else {
243 let hash = Self::lfn_hash(name, suffix);
247 let max_base = 2; if base_len > max_base {
249 base_len = max_base;
250 }
251 base_chars[base_len] = b'~';
252 base_len += 1;
253 for i in (0..4).rev() {
255 let nibble = ((hash >> (i * 4)) & 0xF) as u8;
256 base_chars[base_len] = if nibble < 10 {
257 b'0' + nibble
258 } else {
259 b'A' + nibble - 10
260 };
261 base_len += 1;
262 }
263 }
264 }
265
266 let mut ext_chars = [b' '; 3];
268 let mut ext_len = 0;
269 for ch in ext.chars() {
270 if ext_len >= 3 {
271 break;
272 }
273 let processed = Self::process_char(ch, oem);
274 if processed != 0 {
275 ext_chars[ext_len] = processed;
276 ext_len += 1;
277 }
278 }
279
280 let mut result = [b' '; 11];
282 result[..8].copy_from_slice(&base_chars);
283 result[8..11].copy_from_slice(&ext_chars);
284
285 if base_len == 0 && ext_len == 0 {
287 return Err(CreateShortFileNameError);
288 }
289
290 Self::new(result)
291 }
292
293 #[cfg(feature = "write")]
296 fn lfn_hash(name: &str, suffix: u8) -> u16 {
297 let mut hash: u16 = suffix as u16;
298 for &b in name.as_bytes() {
299 hash = hash.wrapping_mul(37).wrapping_add(b as u16);
300 }
301 hash
302 }
303
304 #[cfg(feature = "write")]
310 fn process_char(ch: char, oem: &dyn crate::oem::OemCpConverter) -> u8 {
311 if ch.is_ascii_alphanumeric() {
312 ch.to_ascii_uppercase() as u8
313 } else if Self::ALLOWED_SYMBOLS.contains(&(ch as u8)) {
314 ch as u8
315 } else if ch == ' ' || ch == '.' {
316 0
318 } else if ch.is_ascii() {
319 b'_'
321 } else {
322 oem.encode(ch).unwrap_or(b'_')
324 }
325 }
326}
327
328pub const LFN_MAX_UTF16_UNITS: usize = 255;
330
331#[cfg(feature = "lfn")]
340#[derive(Clone, PartialEq, Eq)]
341pub struct LongFileName {
342 chars: [u16; LFN_MAX_UTF16_UNITS],
343 len: usize,
344}
345
346#[cfg(feature = "lfn")]
347impl fmt::Debug for LongFileName {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 struct LossyChars<'a>(&'a LongFileName);
350 impl fmt::Debug for LossyChars<'_> {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 f.write_str("\"")?;
353 for ch in self.0.chars() {
354 fmt::Write::write_char(f, ch)?;
355 }
356 f.write_str("\"")
357 }
358 }
359 f.debug_tuple("LongFileName")
360 .field(&LossyChars(self))
361 .finish()
362 }
363}
364
365#[cfg(feature = "lfn")]
366impl Default for LongFileName {
367 fn default() -> Self {
368 Self::new()
369 }
370}
371
372#[cfg(feature = "lfn")]
373impl LongFileName {
374 pub const CHARS_PER_ENTRY: usize = 13;
376
377 pub fn new() -> Self {
379 Self {
380 chars: [0; LFN_MAX_UTF16_UNITS],
381 len: 0,
382 }
383 }
384
385 pub fn clear(&mut self) {
387 self.len = 0;
388 }
389
390 pub fn is_empty(&self) -> bool {
392 self.len == 0
393 }
394
395 pub fn len(&self) -> usize {
397 self.len
398 }
399
400 pub fn prepend_lfn_entry(&mut self, name1: &[u8; 10], name2: &[u8; 12], name3: &[u8; 4]) {
404 let mut utf16_chars = [0u16; Self::CHARS_PER_ENTRY];
406
407 for i in 0..5 {
409 utf16_chars[i] = u16::from_le_bytes([name1[i * 2], name1[i * 2 + 1]]);
410 }
411 for i in 0..6 {
413 utf16_chars[5 + i] = u16::from_le_bytes([name2[i * 2], name2[i * 2 + 1]]);
414 }
415 for i in 0..2 {
417 utf16_chars[11 + i] = u16::from_le_bytes([name3[i * 2], name3[i * 2 + 1]]);
418 }
419
420 let actual_len = utf16_chars
422 .iter()
423 .position(|&c| c == 0x0000 || c == 0xFFFF)
424 .unwrap_or(Self::CHARS_PER_ENTRY);
425
426 let new_len = self.len + actual_len;
428 if new_len > LFN_MAX_UTF16_UNITS {
429 return;
432 }
433 if self.len > 0 {
434 self.chars.copy_within(0..self.len, actual_len);
435 }
436 self.chars[..actual_len].copy_from_slice(&utf16_chars[..actual_len]);
437 self.len = new_len;
438 }
439
440 pub fn as_utf16(&self) -> &[u16] {
442 &self.chars[..self.len]
443 }
444
445 pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
450 char::decode_utf16(self.chars[..self.len].iter().copied())
451 .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER))
452 }
453
454 pub fn eq_str(&self, s: &str) -> bool {
456 self.chars().eq(s.chars())
457 }
458
459 #[cfg(feature = "write")]
465 pub fn from_str_utf16(name: &str) -> Option<Self> {
466 let mut out = Self::new();
467 for ch in name.chars() {
468 let mut tmp = [0u16; 2];
469 for &c in ch.encode_utf16(&mut tmp).iter() {
470 if out.len >= LFN_MAX_UTF16_UNITS {
471 return None;
472 }
473 out.chars[out.len] = c;
474 out.len += 1;
475 }
476 }
477 Some(out)
478 }
479}
480
481#[cfg(feature = "lfn")]
482impl fmt::Display for LongFileName {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 for ch in self.chars() {
485 fmt::Write::write_char(f, ch)?;
486 }
487 Ok(())
488 }
489}
490
491#[cfg(feature = "lfn")]
493pub struct LfnBuilder {
494 pub name: LongFileName,
496 pub checksum: u8,
498 pub expected_seq: u8,
500 pub building: bool,
502}
503
504#[cfg(feature = "lfn")]
505impl Default for LfnBuilder {
506 fn default() -> Self {
507 Self::new()
508 }
509}
510
511#[cfg(feature = "lfn")]
512impl LfnBuilder {
513 pub const LAST_ENTRY_MASK: u8 = 0x40;
515 pub const SEQ_NUMBER_MASK: u8 = 0x3F;
517
518 pub fn new() -> Self {
520 Self {
521 name: LongFileName::new(),
522 checksum: 0,
523 expected_seq: 0,
524 building: false,
525 }
526 }
527
528 pub fn reset(&mut self) {
530 self.name.clear();
531 self.checksum = 0;
532 self.expected_seq = 0;
533 self.building = false;
534 }
535
536 pub fn start(&mut self, seq_number: u8, checksum: u8) {
538 self.reset();
539 self.building = true;
540 self.checksum = checksum;
541 self.expected_seq = seq_number & Self::SEQ_NUMBER_MASK;
543 }
544
545 pub fn add_entry(
548 &mut self,
549 seq_number: u8,
550 checksum: u8,
551 name1: &[u8; 10],
552 name2: &[u8; 12],
553 name3: &[u8; 4],
554 ) -> bool {
555 let seq = seq_number & Self::SEQ_NUMBER_MASK;
556
557 if seq != self.expected_seq {
559 self.reset();
560 return false;
561 }
562
563 if checksum != self.checksum {
565 self.reset();
566 return false;
567 }
568
569 self.name.prepend_lfn_entry(name1, name2, name3);
571
572 self.expected_seq -= 1;
574
575 true
576 }
577
578 pub fn is_complete(&self) -> bool {
580 self.building && self.expected_seq == 0
581 }
582
583 pub fn finish(&mut self, short_name: &ShortFileName) -> Option<LongFileName> {
585 if !self.is_complete() {
586 self.reset();
587 return None;
588 }
589
590 if short_name.lfn_checksum() != self.checksum {
592 self.reset();
593 return None;
594 }
595
596 let result = core::mem::take(&mut self.name);
597 self.reset();
598 Some(result)
599 }
600}
601
602#[cfg(all(test, feature = "lfn", feature = "alloc"))]
603mod lfn_unicode_tests {
604 use super::*;
605 extern crate alloc;
606 use alloc::string::ToString;
607
608 #[test]
614 fn lone_high_surrogate_becomes_replacement_char() {
615 let mut lfn = LongFileName::new();
616 let name1: [u8; 10] = [0x00, 0xD8, b'a', 0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF];
618 let name2: [u8; 12] = [0xFF; 12];
619 let name3: [u8; 4] = [0xFF; 4];
620
621 lfn.prepend_lfn_entry(&name1, &name2, &name3);
622
623 let s = lfn.to_string();
624 assert_eq!(s, "\u{FFFD}a");
625 }
626
627 #[test]
632 fn valid_surrogate_pair_decodes_to_supplementary_codepoint() {
633 let mut lfn = LongFileName::new();
634 let name1: [u8; 10] = [0x3D, 0xD8, 0x00, 0xDE, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF];
636 let name2: [u8; 12] = [0xFF; 12];
637 let name3: [u8; 4] = [0xFF; 4];
638
639 lfn.prepend_lfn_entry(&name1, &name2, &name3);
640
641 assert_eq!(lfn.to_string(), "\u{1F600}");
642 }
643
644 #[test]
650 fn surrogate_pair_split_across_entries() {
651 let mut lfn = LongFileName::new();
652
653 let high_name1: [u8; 10] = [b'a', 0, b'b', 0, b'c', 0, b'd', 0, b'e', 0];
656 let high_name2: [u8; 12] = [b'f', 0, b'g', 0, b'h', 0, b'i', 0, b'j', 0, b'k', 0];
657 let high_name3: [u8; 4] = [b'l', 0, 0x3D, 0xD8]; let low_name1: [u8; 10] = [
662 0x00, 0xDE, b'm', 0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
664 ];
665 let low_name2: [u8; 12] = [0xFF; 12];
666 let low_name3: [u8; 4] = [0xFF; 4];
667
668 lfn.prepend_lfn_entry(&low_name1, &low_name2, &low_name3);
669 lfn.prepend_lfn_entry(&high_name1, &high_name2, &high_name3);
670
671 assert_eq!(lfn.to_string(), "abcdefghijkl\u{1F600}m");
672 }
673
674 #[test]
677 fn two_byte_utf8_codepoint() {
678 let mut lfn = LongFileName::new();
679 let name1: [u8; 10] = [0xE9, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
681 let name2: [u8; 12] = [0xFF; 12];
682 let name3: [u8; 4] = [0xFF; 4];
683
684 lfn.prepend_lfn_entry(&name1, &name2, &name3);
685
686 assert_eq!(lfn.to_string(), "é");
687 }
688
689 #[test]
691 fn eq_str_matches_decoded_chars() {
692 let mut lfn = LongFileName::new();
693 let name1: [u8; 10] = [0x3D, 0xD8, 0x00, 0xDE, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF];
695 let name2: [u8; 12] = [0xFF; 12];
696 let name3: [u8; 4] = [0xFF; 4];
697
698 lfn.prepend_lfn_entry(&name1, &name2, &name3);
699
700 assert!(lfn.eq_str("\u{1F600}"));
701 assert!(!lfn.eq_str("X"));
702 }
703}