1use super::io::{self, Read, Write};
2use bytemuck::Zeroable;
3
4use super::io::LogicalSector;
5use crate::types::{U16LsbMsb, U32LsbMsb};
6
7#[repr(C)]
16#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
17pub struct DirectoryRecordHeader {
18 pub len: u8,
20 pub extended_attr_record: u8,
22 pub extent: U32LsbMsb,
24 pub data_len: U32LsbMsb,
26 pub date_time: DirDateTime,
28 pub flags: u8,
30 pub file_unit_size: u8,
32 pub interleave_gap_size: u8,
34 pub volume_sequence_number: U16LsbMsb,
36 pub file_identifier_len: u8,
38}
39
40impl Default for DirectoryRecordHeader {
41 fn default() -> Self {
42 Self {
43 len: 0,
44 extended_attr_record: 0,
45 extent: U32LsbMsb::new(0),
46 data_len: U32LsbMsb::new(0),
47 date_time: DirDateTime::now(),
48 flags: 0,
49 file_unit_size: 0,
50 interleave_gap_size: 0,
51 volume_sequence_number: U16LsbMsb::new(0),
52 file_identifier_len: 0,
53 }
54 }
55}
56
57impl DirectoryRecordHeader {
58 pub fn from_bytes(bytes: &[u8]) -> &Self {
60 bytemuck::from_bytes(bytes)
61 }
62
63 pub fn to_bytes(&self) -> &[u8] {
65 bytemuck::bytes_of(self)
66 }
67
68 pub fn is_directory(&self) -> bool {
70 FileFlags::from_bits_retain(self.flags).contains(FileFlags::DIRECTORY)
71 }
72}
73
74#[repr(transparent)]
82#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
83pub struct DirectoryRecord {
84 data: [u8; 256],
85}
86
87impl Default for DirectoryRecord {
88 fn default() -> Self {
89 bytemuck::Zeroable::zeroed()
90 }
91}
92
93#[derive(Debug, Clone, Copy)]
95pub struct NotADirectoryError;
96
97impl core::fmt::Display for NotADirectoryError {
98 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99 write!(f, "not a directory")
100 }
101}
102
103#[cfg(feature = "std")]
104impl std::error::Error for NotADirectoryError {}
105
106impl DirectoryRecord {
107 const DATA_START: usize = size_of::<DirectoryRecordHeader>();
108
109 #[inline]
110 pub fn header(&self) -> &DirectoryRecordHeader {
112 bytemuck::from_bytes(&self.data[0..Self::DATA_START])
113 }
114
115 #[inline]
116 #[allow(dead_code)]
118 pub(crate) fn len(&self) -> u8 {
119 let header = self.header();
120 header.len
121 }
122
123 #[allow(dead_code)]
125 pub(crate) fn is_empty(&self) -> bool {
126 self.len() == 0
127 }
128
129 #[inline]
130 #[allow(dead_code)]
132 pub(crate) fn flags(&self) -> FileFlags {
133 let header = self.header();
134 FileFlags::from_bits_truncate(header.flags)
135 }
136
137 #[inline]
138 pub fn header_mut(&mut self) -> &mut DirectoryRecordHeader {
140 bytemuck::from_bytes_mut(&mut self.data[0..size_of::<DirectoryRecordHeader>()])
141 }
142
143 #[inline]
144 pub fn name(&self) -> &[u8] {
146 let len = self.header().file_identifier_len as usize;
147 &self.data[Self::DATA_START..Self::DATA_START + len]
148 }
149
150 #[cfg(feature = "alloc")]
155 pub fn joliet_name(&self) -> alloc::string::String {
156 crate::joliet::decode_joliet_name(self.name())
157 }
158
159 #[cfg(feature = "alloc")]
161 pub fn is_joliet_name(&self) -> bool {
162 crate::joliet::is_likely_joliet_name(self.name())
163 }
164
165 #[inline]
166 pub fn system_use(&self) -> &[u8] {
168 let header = self.header();
169 let su_start = (Self::DATA_START + header.file_identifier_len as usize + 1) & !1;
172 if su_start >= header.len as usize {
173 return &[];
174 }
175 &self.data[su_start..header.len as usize]
176 }
177
178 sync_only! {
179 #[cfg(feature = "write")]
181 pub(crate) fn system_use_mut(&mut self) -> &mut [u8] {
182 let header = self.header();
183 let name_len = header.file_identifier_len as usize;
184 let start = (Self::DATA_START + name_len + 1) & !1;
185 let end = header.len as usize;
186 &mut self.data[start..end]
187 }
188 }
189
190 #[inline]
191 pub fn is_special(&self) -> bool {
193 self.name() == b"\x00" || self.name() == b"\x01"
194 }
195
196 #[inline]
197 pub fn is_directory(&self) -> bool {
199 self.header().is_directory()
200 }
201
202 pub fn is_file(&self) -> bool {
204 !self.header().is_directory()
205 }
206
207 pub fn as_dir_ref(&self) -> Result<DirectoryRef, NotADirectoryError> {
209 if !self.is_directory() {
210 return Err(NotADirectoryError);
211 }
212
213 let header = self.header();
214 Ok(DirectoryRef {
215 extent: LogicalSector(header.extent.read() as usize),
216 size: header.data_len.read() as usize,
217 })
218 }
219
220 pub fn size(&self) -> usize {
222 self.header().len as usize
223 }
224
225 pub fn to_bytes(&self) -> &[u8] {
227 &self.data[0..self.size()]
228 }
229
230 pub fn new(name: &[u8], system_use: &[u8], directory: DirectoryRef, flags: FileFlags) -> Self {
232 let mut sel = Self::zeroed();
233 assert!(
234 !name.is_empty() && name.len() <= u8::MAX as usize,
235 "ISO directory-record identifier must contain 1..=255 bytes"
236 );
237 let su_start = (Self::DATA_START + name.len() + 1) & !1;
240 let total = su_start + system_use.len();
241 let record_len = (total + 1) & !1;
243 assert!(
244 record_len <= 255,
245 "DirectoryRecord too large: {} bytes (name={}, su={})",
246 record_len,
247 name.len(),
248 system_use.len()
249 );
250 *sel.header_mut() = DirectoryRecordHeader {
251 len: record_len as u8,
252 extended_attr_record: 0,
253 extent: U32LsbMsb::new(directory.extent.0 as u32),
254 data_len: U32LsbMsb::new(directory.size as u32),
255 date_time: DirDateTime::now(),
256 flags: flags.bits(),
257 file_unit_size: 0,
258 interleave_gap_size: 0,
259 volume_sequence_number: U16LsbMsb::new(1),
260 file_identifier_len: name.len() as u8,
261 };
262 sel.data[Self::DATA_START..Self::DATA_START + name.len()].copy_from_slice(name);
263 sel.data[su_start..su_start + system_use.len()].copy_from_slice(system_use);
265 sel
266 }
267
268 pub fn with_len(name_len: usize, su_len: usize) -> Self {
270 let mut sel = Self::zeroed();
271 assert!(
272 (1..=u8::MAX as usize).contains(&name_len),
273 "ISO directory-record identifier must contain 1..=255 bytes"
274 );
275 let su_start = (Self::DATA_START + name_len + 1) & !1;
276 let total = su_start + su_len;
277 let record_len = (total + 1) & !1;
278 assert!(record_len <= u8::MAX as usize, "DirectoryRecord too large");
279 sel.header_mut().len = record_len as u8;
280 sel
281 }
282}
283
284io_transform! {
285impl DirectoryRecord {
286 pub async fn parse<R: Read>(reader: &mut R) -> io::Result<Self> {
288 let mut sel = Self::zeroed();
289 reader.read_exact(&mut sel.data[0..Self::DATA_START]).await?;
290 let size = sel.size();
291 if size == 0 {
292 return Ok(sel);
293 }
294 let name_len = sel.header().file_identifier_len as usize;
295 if size < Self::DATA_START + 1
296 || !size.is_multiple_of(2)
297 || Self::DATA_START + name_len > size
298 || sel.header().flags & 0b0110_0000 != 0
299 || !sel.header().extent.is_consistent()
300 || !sel.header().data_len.is_consistent()
301 || !sel.header().volume_sequence_number.is_consistent()
302 {
303 return Err(io::Error::new(
304 io::ErrorKind::InvalidData,
305 "invalid ISO directory record",
306 ));
307 }
308 if size > Self::DATA_START {
309 reader.read_exact(&mut sel.data[Self::DATA_START..size]).await?;
310 }
311 if name_len.is_multiple_of(2) {
312 let padding = Self::DATA_START + name_len;
313 if padding >= size || sel.data[padding] != 0 {
314 return Err(io::Error::new(
315 io::ErrorKind::InvalidData,
316 "invalid ISO directory-record identifier padding",
317 ));
318 }
319 }
320 Ok(sel)
321 }
322
323 pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<usize> {
325 let size = self.size();
326 writer.write_all(&self.data[0..size]).await?;
327 Ok(size)
328 }
329}
330} #[repr(C)]
334#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
335pub struct RootDirectoryEntry {
336 pub header: DirectoryRecordHeader,
338 pub padding: u8,
340}
341
342#[repr(C)]
343#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
344pub struct DirDateTime {
346 year: u8,
348 month: u8,
349 day: u8,
350 hour: u8,
351 minute: u8,
352 second: u8,
353 offset: u8,
354}
355
356impl DirDateTime {
357 #[cfg(feature = "std")]
358 pub fn now() -> Self {
360 use chrono::{Datelike, Timelike, Utc};
361 let now = Utc::now();
362 Self {
363 year: (now.year() - 1900) as u8,
364 month: now.month() as u8,
365 day: now.day() as u8,
366 hour: now.hour() as u8,
367 minute: now.minute() as u8,
368 second: now.second() as u8,
369 offset: 0,
371 }
372 }
373
374 #[cfg(not(feature = "std"))]
376 pub fn now() -> Self {
377 Self::default()
378 }
379}
380
381#[derive(Default, Debug, Clone, Copy)]
385pub struct DirectoryRef {
386 pub extent: LogicalSector,
388 pub size: usize,
390}
391
392impl DirectoryRef {
393 #[allow(dead_code)]
395 pub(crate) const fn new(extent: usize, size: usize) -> Self {
396 Self {
397 extent: LogicalSector(extent),
398 size,
399 }
400 }
401}
402
403sync_only! {
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use alloc::vec;
408 use alloc::vec::Vec;
409 use std::io::Cursor;
410
411 #[test]
413 fn directory_record_parse_roundtrip() {
414 let mut dir = vec![0u8; 2048];
416 let mut offset = 0usize;
417 for (id, flags) in [(0x00u8, 0x02u8), (0x01u8, 0x02u8)] {
418 dir[offset] = 34;
419 dir[offset + 2..offset + 6].copy_from_slice(&20u32.to_le_bytes());
420 dir[offset + 6..offset + 10].copy_from_slice(&20u32.to_be_bytes());
421 dir[offset + 10..offset + 14].copy_from_slice(&2048u32.to_le_bytes());
422 dir[offset + 14..offset + 18].copy_from_slice(&2048u32.to_be_bytes());
423 dir[offset + 25] = flags;
424 dir[offset + 28..offset + 30].copy_from_slice(&1u16.to_le_bytes());
425 dir[offset + 30..offset + 32].copy_from_slice(&1u16.to_be_bytes());
426 dir[offset + 32] = 1;
427 dir[offset + 33] = id;
428 offset += 34;
429 }
430
431 let mut cursor = Cursor::new(&dir[..]);
432 let dot = DirectoryRecord::parse(&mut cursor).expect("parse .");
433 assert_eq!(dot.size(), 34);
434 assert_eq!(core::mem::size_of::<DirectoryRecordHeader>(), 33);
435 assert!(dot.is_directory());
436 assert_eq!(dot.name(), b"\x00");
437 assert_eq!(dot.header().extent.read(), 20);
438 assert_eq!(dot.header().data_len.read(), 2048);
439
440 let dotdot = DirectoryRecord::parse(&mut cursor).expect("parse ..");
441 assert_eq!(dotdot.name(), b"\x01");
442 assert!(dotdot.is_special());
443
444 let made = DirectoryRecord::new(
446 b"README.;1",
447 &[],
448 DirectoryRef {
449 extent: LogicalSector(42),
450 size: 100,
451 },
452 FileFlags::empty(),
453 );
454 assert!(made.size() >= 33 + b"README.;1".len());
455 assert!(!made.is_directory());
456 assert_eq!(made.name(), b"README.;1");
457 assert_eq!(made.header().extent.read(), 42);
458
459 let mut out = Vec::new();
460 made.write(&mut out).expect("write");
461 let mut round = Cursor::new(&out[..]);
462 let parsed = DirectoryRecord::parse(&mut round).expect("re-parse");
463 assert_eq!(parsed.name(), made.name());
464 assert_eq!(parsed.size(), made.size());
465 assert_eq!(parsed.header().extent.read(), 42);
466 }
467
468 #[test]
469 fn directory_record_rejects_invalid_bounds_and_endian_copy() {
470 let mut short = vec![0_u8; 33];
471 short[0] = 32;
472 short[32] = 1;
473 assert_eq!(
474 DirectoryRecord::parse(&mut Cursor::new(short))
475 .unwrap_err()
476 .kind(),
477 io::ErrorKind::InvalidData
478 );
479
480 let mut mismatched = vec![0_u8; 34];
481 mismatched[0] = 34;
482 mismatched[2..6].copy_from_slice(&20_u32.to_le_bytes());
483 mismatched[6..10].copy_from_slice(&21_u32.to_be_bytes());
484 mismatched[28..30].copy_from_slice(&1_u16.to_le_bytes());
485 mismatched[30..32].copy_from_slice(&1_u16.to_be_bytes());
486 mismatched[32] = 1;
487 assert_eq!(
488 DirectoryRecord::parse(&mut Cursor::new(mismatched))
489 .unwrap_err()
490 .kind(),
491 io::ErrorKind::InvalidData
492 );
493 }
494}
495}
496
497bitflags::bitflags! {
498 #[derive(Debug, Clone, Copy)]
500 pub struct FileFlags: u8 {
501 const HIDDEN = 0b0000_0001;
503 const DIRECTORY = 0b0000_0010;
505 const ASSOCIATED_FILE = 0b0000_0100;
507 const EXTENDED_ATTRIBUTES = 0b0000_1000;
509 const EXTENDED_PERMISSIONS = 0b0001_0000;
511 const NOT_FINAL = 0b1000_0000;
513 }
514}