use hadris_fixed::FixedBytes;
#[cfg(feature = "alloc")]
use crate::joliet::JolietLevel;
#[cfg(feature = "write")]
use crate::types::{Charset, CharsetD, CharsetD1};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EntryType {
Level1 {
supports_lowercase: bool,
supports_rrip: bool,
},
Level2 {
supports_lowercase: bool,
supports_rrip: bool,
},
Level3 {
supports_lowercase: bool,
supports_rrip: bool,
},
#[cfg(feature = "alloc")]
Joliet {
level: JolietLevel,
supports_rrip: bool,
},
}
impl Default for EntryType {
fn default() -> Self {
Self::Level1 {
supports_lowercase: false,
supports_rrip: false,
}
}
}
impl EntryType {
pub fn supports_rrip(&self) -> bool {
match self {
Self::Level1 { supports_rrip, .. } => *supports_rrip,
Self::Level2 { supports_rrip, .. } => *supports_rrip,
Self::Level3 { supports_rrip, .. } => *supports_rrip,
#[cfg(feature = "alloc")]
Self::Joliet { supports_rrip, .. } => *supports_rrip,
}
}
fn usefulness(self) -> u8 {
match self {
Self::Level1 {
supports_lowercase,
supports_rrip,
} => (supports_lowercase as u8) << 2 | (supports_rrip as u8) << 4,
Self::Level2 {
supports_lowercase,
supports_rrip,
} => 0x01 | (supports_lowercase as u8) << 2 | (supports_rrip as u8) << 4,
Self::Level3 {
supports_lowercase,
supports_rrip,
} => 0x02 | (supports_lowercase as u8) << 2 | (supports_rrip as u8) << 4,
#[cfg(feature = "alloc")]
Self::Joliet {
level,
supports_rrip,
} => (level as u8 + 11) | (supports_rrip as u8) << 4,
}
}
}
impl Ord for EntryType {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.usefulness().cmp(&other.usefulness())
}
}
impl PartialOrd for EntryType {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[cfg(feature = "alloc")]
impl From<JolietLevel> for EntryType {
fn from(value: JolietLevel) -> Self {
Self::Joliet {
level: value,
supports_rrip: false,
}
}
}
pub type FilenameL1 = FixedBytes<14>;
pub type FilenameL2 = FixedBytes<32>;
pub type FilenameL3 = FixedBytes<207>;
#[cfg(feature = "write")]
pub enum ConvertedName {
Level1(FilenameL1),
Level2(FilenameL2),
Level3(FilenameL3),
Joliet(FixedBytes<207>),
}
#[cfg(feature = "write")]
impl ConvertedName {
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Level1(name) => name.as_bytes(),
Self::Level2(name) => name.as_bytes(),
Self::Level3(name) => name.as_bytes(),
Self::Joliet(name) => name.as_bytes(),
}
}
}
#[cfg(feature = "write")]
impl EntryType {
pub fn convert_name(self, name: &str) -> ConvertedName {
match self {
Self::Level1 {
supports_lowercase, ..
} => ConvertedName::Level1(convert_l1(name, supports_lowercase)),
Self::Level2 {
supports_lowercase, ..
} => ConvertedName::Level2(convert_l2(name, supports_lowercase)),
Self::Level3 {
supports_lowercase, ..
} => ConvertedName::Level3(convert_l3(name, supports_lowercase)),
Self::Joliet { level, .. } => match level {
JolietLevel::Level1 | JolietLevel::Level2 | JolietLevel::Level3 => {
ConvertedName::Joliet(convert_joliet3(name))
}
},
}
}
pub fn convert_directory_name(self, name: &str) -> ConvertedName {
fn primary<const N: usize>(
name: &str,
max: usize,
supports_lowercase: bool,
) -> FixedBytes<N> {
let mut converted = FixedBytes::empty();
let end = name
.char_indices()
.map(|(offset, _)| offset)
.take_while(|offset| *offset <= max)
.last()
.unwrap_or(0);
let end = if name.len() <= max { name.len() } else { end };
let range = converted.push_slice(&name.as_bytes()[..end]);
let bytes = converted.as_bytes_mut()[range].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(bytes);
} else {
CharsetD::substitute_invalid(bytes);
}
converted
}
match self {
Self::Level1 {
supports_lowercase, ..
} => ConvertedName::Level1(primary(name, 8, supports_lowercase)),
Self::Level2 {
supports_lowercase, ..
} => ConvertedName::Level2(primary(name, 31, supports_lowercase)),
Self::Level3 {
supports_lowercase, ..
} => ConvertedName::Level3(primary(name, 31, supports_lowercase)),
Self::Joliet { .. } => ConvertedName::Joliet(convert_joliet3(name)),
}
}
}
#[cfg(feature = "write")]
pub fn convert_l1(name: &str, supports_lowercase: bool) -> FixedBytes<14> {
let mut l1 = FixedBytes::empty();
let name_bytes = name.as_bytes();
match name.find('.') {
Some(index) => {
let basename = l1.push_slice(&name_bytes[0..index.min(8)]);
let basename = l1.as_bytes_mut()[basename].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(basename);
} else {
CharsetD::substitute_invalid(basename);
}
let ext_len = (name.len() - index - 1).min(3);
l1.push_byte(b'.');
let ext = l1.push_slice(&name_bytes[index + 1..(index + 1 + ext_len).min(name.len())]);
let ext = l1.as_bytes_mut()[ext].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(ext);
} else {
CharsetD::substitute_invalid(ext);
}
}
None => {
let len = name.len().min(8);
let basename = l1.push_slice(&name_bytes[0..len]);
let basename = l1.as_bytes_mut()[basename].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(basename);
} else {
CharsetD::substitute_invalid(basename);
}
}
}
l1.push_slice(b";1");
l1
}
#[cfg(feature = "write")]
pub fn convert_l2(name: &str, supports_lowercase: bool) -> FilenameL2 {
let mut l2 = FilenameL2::empty();
let name_bytes = name.as_bytes();
const MAX_NAME_LEN: usize = 30;
match name.find('.') {
Some(index) => {
let basename_end = index.min(MAX_NAME_LEN);
let basename = l2.push_slice(&name_bytes[0..basename_end]);
let basename = l2.as_bytes_mut()[basename].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(basename);
} else {
CharsetD::substitute_invalid(basename);
}
let remaining = MAX_NAME_LEN.saturating_sub(basename_end + 1);
if remaining > 0 {
l2.push_byte(b'.');
let ext_end = (index + 1 + remaining).min(name.len());
let ext = l2.push_slice(&name_bytes[index + 1..ext_end]);
let ext = l2.as_bytes_mut()[ext].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(ext);
} else {
CharsetD::substitute_invalid(ext);
}
}
}
None => {
let len = name.len().min(MAX_NAME_LEN);
let basename = l2.push_slice(&name_bytes[0..len]);
let basename = l2.as_bytes_mut()[basename].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(basename);
} else {
CharsetD::substitute_invalid(basename);
}
}
}
l2.push_slice(b";1");
l2
}
#[cfg(feature = "write")]
pub fn convert_l3(name: &str, supports_lowercase: bool) -> FilenameL3 {
let mut l3 = FilenameL3::empty();
let name_bytes = name.as_bytes();
const MAX_NAME_LEN: usize = 207;
match name.find('.') {
Some(index) => {
let basename_end = index.min(MAX_NAME_LEN);
let basename = l3.push_slice(&name_bytes[0..basename_end]);
let basename = l3.as_bytes_mut()[basename].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(basename);
} else {
CharsetD::substitute_invalid(basename);
}
let remaining = MAX_NAME_LEN.saturating_sub(basename_end + 1);
if remaining > 0 {
l3.push_byte(b'.');
let ext_end = (index + 1 + remaining).min(name.len());
let ext = l3.push_slice(&name_bytes[index + 1..ext_end]);
let ext = l3.as_bytes_mut()[ext].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(ext);
} else {
CharsetD::substitute_invalid(ext);
}
}
}
None => {
let len = name.len().min(MAX_NAME_LEN);
let basename = l3.push_slice(&name_bytes[0..len]);
let basename = l3.as_bytes_mut()[basename].iter_mut();
if supports_lowercase {
CharsetD1::substitute_invalid(basename);
} else {
CharsetD::substitute_invalid(basename);
}
}
}
l3
}
#[cfg(feature = "write")]
pub const JOLIET_MAX_NAME_CHARS: usize = 64;
#[cfg(feature = "write")]
pub fn convert_joliet3(name: &str) -> FixedBytes<207> {
let mut j1 = FixedBytes::empty();
for c in name.chars().take(JOLIET_MAX_NAME_CHARS) {
let unit = if (c as u32) <= 0xFFFF {
c as u16
} else {
b'_' as u16
};
j1.push_slice(&unit.to_be_bytes());
}
j1
}
#[cfg(all(test, feature = "write", feature = "alloc"))]
mod joliet_tests {
use super::*;
use crate::joliet::decode_joliet_name;
#[test]
fn joliet_name_is_truncated_to_the_conformant_limit() {
let long = "a".repeat(200);
let encoded = convert_joliet3(&long);
assert_eq!(encoded.as_bytes().len(), JOLIET_MAX_NAME_CHARS * 2);
assert_eq!(decode_joliet_name(encoded.as_bytes()).chars().count(), 64);
}
#[test]
fn joliet_substitutes_non_bmp_instead_of_emitting_surrogates() {
let encoded = convert_joliet3("a\u{1F600}b.txt");
let decoded = decode_joliet_name(encoded.as_bytes());
assert_eq!(decoded, "a_b.txt");
for pair in encoded.as_bytes().chunks_exact(2) {
let unit = u16::from_be_bytes([pair[0], pair[1]]);
assert!(
!(0xD800..=0xDFFF).contains(&unit),
"surrogate leaked into UCS-2"
);
}
}
#[test]
fn joliet_preserves_bmp_unicode() {
let encoded = convert_joliet3("日本語.txt");
assert_eq!(decode_joliet_name(encoded.as_bytes()), "日本語.txt");
}
}