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 pub fn header_mut(&mut self) -> &mut DirectoryRecordHeader {
118 bytemuck::from_bytes_mut(&mut self.data[0..size_of::<DirectoryRecordHeader>()])
119 }
120
121 #[inline]
122 pub fn name(&self) -> &[u8] {
124 let len = self.header().file_identifier_len as usize;
125 &self.data[Self::DATA_START..Self::DATA_START + len]
126 }
127
128 #[cfg(feature = "alloc")]
133 pub fn joliet_name(&self) -> alloc::string::String {
134 crate::joliet::decode_joliet_name(self.name())
135 }
136
137 #[cfg(feature = "alloc")]
139 pub fn is_joliet_name(&self) -> bool {
140 crate::joliet::is_likely_joliet_name(self.name())
141 }
142
143 #[inline]
144 pub fn system_use(&self) -> &[u8] {
146 let header = self.header();
147 let su_start = (Self::DATA_START + header.file_identifier_len as usize + 1) & !1;
150 if su_start >= header.len as usize {
151 return &[];
152 }
153 &self.data[su_start..header.len as usize]
154 }
155
156 sync_only! {
157 #[cfg(feature = "write")]
159 pub(crate) fn system_use_mut(&mut self) -> &mut [u8] {
160 let name_len = self.header().file_identifier_len as usize;
161 let start = (Self::DATA_START + name_len + 1) & !1;
162 let end = self.header().len as usize;
163 &mut self.data[start..end]
164 }
165 }
166
167 #[inline]
168 pub fn is_special(&self) -> bool {
170 self.name() == b"\x00" || self.name() == b"\x01"
171 }
172
173 #[inline]
174 pub fn is_directory(&self) -> bool {
176 self.header().is_directory()
177 }
178
179 pub fn is_file(&self) -> bool {
181 !self.header().is_directory()
182 }
183
184 pub fn as_dir_ref(&self) -> Result<DirectoryRef, NotADirectoryError> {
186 if !self.is_directory() {
187 return Err(NotADirectoryError);
188 }
189
190 let header = self.header();
191 Ok(DirectoryRef {
192 extent: LogicalSector(header.extent.read() as usize),
193 size: header.data_len.read() as usize,
194 })
195 }
196
197 pub fn size(&self) -> usize {
199 self.header().len as usize
200 }
201
202 pub fn to_bytes(&self) -> &[u8] {
204 &self.data[0..self.size()]
205 }
206
207 pub fn new(name: &[u8], system_use: &[u8], directory: DirectoryRef, flags: FileFlags) -> Self {
209 let mut sel = Self::zeroed();
210 assert!(
211 !name.is_empty() && name.len() <= u8::MAX as usize,
212 "ISO directory-record identifier must contain 1..=255 bytes"
213 );
214 let su_start = (Self::DATA_START + name.len() + 1) & !1;
217 let total = su_start + system_use.len();
218 let record_len = (total + 1) & !1;
220 assert!(
221 record_len <= 255,
222 "DirectoryRecord too large: {} bytes (name={}, su={})",
223 record_len,
224 name.len(),
225 system_use.len()
226 );
227 *sel.header_mut() = DirectoryRecordHeader {
228 len: record_len as u8,
229 extended_attr_record: 0,
230 extent: U32LsbMsb::new(directory.extent.0 as u32),
231 data_len: U32LsbMsb::new(directory.size as u32),
232 date_time: DirDateTime::now(),
233 flags: flags.bits(),
234 file_unit_size: 0,
235 interleave_gap_size: 0,
236 volume_sequence_number: U16LsbMsb::new(1),
237 file_identifier_len: name.len() as u8,
238 };
239 sel.data[Self::DATA_START..Self::DATA_START + name.len()].copy_from_slice(name);
240 sel.data[su_start..su_start + system_use.len()].copy_from_slice(system_use);
242 sel
243 }
244
245 pub fn with_len(name_len: usize, su_len: usize) -> Self {
247 let mut sel = Self::zeroed();
248 assert!(
249 (1..=u8::MAX as usize).contains(&name_len),
250 "ISO directory-record identifier must contain 1..=255 bytes"
251 );
252 let su_start = (Self::DATA_START + name_len + 1) & !1;
253 let total = su_start + su_len;
254 let record_len = (total + 1) & !1;
255 assert!(record_len <= u8::MAX as usize, "DirectoryRecord too large");
256 sel.header_mut().len = record_len as u8;
257 sel
258 }
259}
260
261io_transform! {
262impl DirectoryRecord {
263 pub async fn parse<R: Read>(reader: &mut R) -> io::Result<Self> {
265 let mut sel = Self::zeroed();
266 reader.read_exact(&mut sel.data[0..Self::DATA_START]).await?;
267 let size = sel.size();
268 if size == 0 {
269 return Ok(sel);
270 }
271 let name_len = sel.header().file_identifier_len as usize;
272 if size < Self::DATA_START + 1
273 || !size.is_multiple_of(2)
274 || Self::DATA_START + name_len > size
275 || sel.header().flags & 0b0110_0000 != 0
276 || !sel.header().extent.is_consistent()
277 || !sel.header().data_len.is_consistent()
278 || !sel.header().volume_sequence_number.is_consistent()
279 {
280 return Err(io::Error::new(
281 io::ErrorKind::InvalidData,
282 "invalid ISO directory record",
283 ));
284 }
285 if size > Self::DATA_START {
286 reader.read_exact(&mut sel.data[Self::DATA_START..size]).await?;
287 }
288 if name_len.is_multiple_of(2) {
289 let padding = Self::DATA_START + name_len;
290 if padding >= size || sel.data[padding] != 0 {
291 return Err(io::Error::new(
292 io::ErrorKind::InvalidData,
293 "invalid ISO directory-record identifier padding",
294 ));
295 }
296 }
297 Ok(sel)
298 }
299
300 pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<usize> {
302 let size = self.size();
303 writer.write_all(&self.data[0..size]).await?;
304 Ok(size)
305 }
306}
307} #[repr(C)]
311#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
312pub struct RootDirectoryEntry {
313 pub header: DirectoryRecordHeader,
315 pub padding: u8,
317}
318
319#[repr(C)]
320#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
321pub struct DirDateTime {
323 year: u8,
325 month: u8,
326 day: u8,
327 hour: u8,
328 minute: u8,
329 second: u8,
330 offset: u8,
331}
332
333impl DirDateTime {
334 #[cfg(feature = "std")]
335 pub fn now() -> Self {
337 use chrono::{Datelike, Timelike, Utc};
338 let now = Utc::now();
339 Self {
340 year: (now.year() - 1900) as u8,
341 month: now.month() as u8,
342 day: now.day() as u8,
343 hour: now.hour() as u8,
344 minute: now.minute() as u8,
345 second: now.second() as u8,
346 offset: 0,
348 }
349 }
350
351 #[cfg(not(feature = "std"))]
353 pub fn now() -> Self {
354 Self::default()
355 }
356}
357
358#[derive(Default, Debug, Clone, Copy)]
359pub struct DirectoryRef {
361 pub extent: LogicalSector,
363 pub size: usize,
365}
366
367sync_only! {
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use alloc::vec;
372 use alloc::vec::Vec;
373 use std::io::Cursor;
374
375 #[test]
377 fn directory_record_parse_roundtrip() {
378 let mut dir = vec![0u8; 2048];
380 let mut offset = 0usize;
381 for (id, flags) in [(0x00u8, 0x02u8), (0x01u8, 0x02u8)] {
382 dir[offset] = 34;
383 dir[offset + 2..offset + 6].copy_from_slice(&20u32.to_le_bytes());
384 dir[offset + 6..offset + 10].copy_from_slice(&20u32.to_be_bytes());
385 dir[offset + 10..offset + 14].copy_from_slice(&2048u32.to_le_bytes());
386 dir[offset + 14..offset + 18].copy_from_slice(&2048u32.to_be_bytes());
387 dir[offset + 25] = flags;
388 dir[offset + 28..offset + 30].copy_from_slice(&1u16.to_le_bytes());
389 dir[offset + 30..offset + 32].copy_from_slice(&1u16.to_be_bytes());
390 dir[offset + 32] = 1;
391 dir[offset + 33] = id;
392 offset += 34;
393 }
394
395 let mut cursor = Cursor::new(&dir[..]);
396 let dot = DirectoryRecord::parse(&mut cursor).expect("parse .");
397 assert_eq!(dot.size(), 34);
398 assert_eq!(core::mem::size_of::<DirectoryRecordHeader>(), 33);
399 assert!(dot.is_directory());
400 assert_eq!(dot.name(), b"\x00");
401 assert_eq!(dot.header().extent.read(), 20);
402 assert_eq!(dot.header().data_len.read(), 2048);
403
404 let dotdot = DirectoryRecord::parse(&mut cursor).expect("parse ..");
405 assert_eq!(dotdot.name(), b"\x01");
406 assert!(dotdot.is_special());
407
408 let made = DirectoryRecord::new(
410 b"README.;1",
411 &[],
412 DirectoryRef {
413 extent: LogicalSector(42),
414 size: 100,
415 },
416 FileFlags::empty(),
417 );
418 assert!(made.size() >= 33 + b"README.;1".len());
419 assert!(!made.is_directory());
420 assert_eq!(made.name(), b"README.;1");
421 assert_eq!(made.header().extent.read(), 42);
422
423 let mut out = Vec::new();
424 made.write(&mut out).expect("write");
425 let mut round = Cursor::new(&out[..]);
426 let parsed = DirectoryRecord::parse(&mut round).expect("re-parse");
427 assert_eq!(parsed.name(), made.name());
428 assert_eq!(parsed.size(), made.size());
429 assert_eq!(parsed.header().extent.read(), 42);
430 }
431
432 #[test]
433 fn directory_record_rejects_invalid_bounds_and_endian_copy() {
434 let mut short = vec![0_u8; 33];
435 short[0] = 32;
436 short[32] = 1;
437 assert_eq!(
438 DirectoryRecord::parse(&mut Cursor::new(short))
439 .unwrap_err()
440 .kind(),
441 io::ErrorKind::InvalidData
442 );
443
444 let mut mismatched = vec![0_u8; 34];
445 mismatched[0] = 34;
446 mismatched[2..6].copy_from_slice(&20_u32.to_le_bytes());
447 mismatched[6..10].copy_from_slice(&21_u32.to_be_bytes());
448 mismatched[28..30].copy_from_slice(&1_u16.to_le_bytes());
449 mismatched[30..32].copy_from_slice(&1_u16.to_be_bytes());
450 mismatched[32] = 1;
451 assert_eq!(
452 DirectoryRecord::parse(&mut Cursor::new(mismatched))
453 .unwrap_err()
454 .kind(),
455 io::ErrorKind::InvalidData
456 );
457 }
458}
459}
460
461bitflags::bitflags! {
462 #[derive(Clone, Copy)]
464 pub struct FileFlags: u8 {
465 const HIDDEN = 0b0000_0001;
467 const DIRECTORY = 0b0000_0010;
469 const ASSOCIATED_FILE = 0b0000_0100;
471 const EXTENDED_ATTRIBUTES = 0b0000_1000;
473 const EXTENDED_PERMISSIONS = 0b0001_0000;
475 const NOT_FINAL = 0b1000_0000;
477 }
478}