cbm/disk/
directory.rs

1//! CBM DOS directories
2
3use std::fmt;
4use std::fmt::Write;
5use std::io;
6use std::iter;
7
8use disk::block::{Location, Position, PositionedData, BLOCK_SIZE};
9use disk::chain::{ChainIterator, ChainSector};
10use disk::file::Scheme;
11use disk::geos::{GEOSFileStructure, GEOSFileType};
12use disk::{Disk, DiskError, PADDING_BYTE};
13use petscii::Petscii;
14
15const FILE_TYPE_DEL: u8 = 0x00;
16const FILE_TYPE_SEQ: u8 = 0x01;
17const FILE_TYPE_PRG: u8 = 0x02;
18const FILE_TYPE_USR: u8 = 0x03;
19const FILE_TYPE_REL: u8 = 0x04;
20const FILE_ATTRIB_FILE_TYPE_MASK: u8 = 0x0F;
21const FILE_ATTRIB_UNUSED_MASK: u8 = 0x10;
22const FILE_ATTRIB_SAVE_WITH_REPLACE_MASK: u8 = 0x20;
23const FILE_ATTRIB_LOCKED_MASK: u8 = 0x40;
24const FILE_ATTRIB_CLOSED_MASK: u8 = 0x80;
25
26/// A directory entry categorizes files as SEQ, PRG, USR, or REL, along with a
27/// pseudo-file-type of DEL to indicate deleted files.
28#[derive(PartialEq, Debug, Clone, Copy)]
29pub enum FileType {
30    DEL,
31    SEQ,
32    PRG,
33    USR,
34    REL,
35    Unknown(u8),
36}
37
38impl FileType {
39    /// Return a string representation of this file type.
40    pub fn from_string(string: &str) -> Option<FileType> {
41        match string.to_uppercase().as_str() {
42            "DEL" => Some(FileType::DEL),
43            "SEQ" => Some(FileType::SEQ),
44            "PRG" => Some(FileType::PRG),
45            "USR" => Some(FileType::USR),
46            "REL" => Some(FileType::REL),
47            _ => None,
48        }
49    }
50}
51
52impl fmt::Display for FileType {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        f.write_str(match self {
55            &FileType::DEL => "del",
56            &FileType::SEQ => "seq",
57            &FileType::PRG => "prg",
58            &FileType::USR => "usr",
59            &FileType::REL => "rel",
60            &FileType::Unknown(_) => "unk",
61        })
62    }
63}
64
65/// We introduce the term "file attributes" to refer to the full 8-bit
66/// directory entry field which contains the file type along with several flags.
67#[derive(Clone)]
68pub struct FileAttributes {
69    /// Bits 0-3 indicate the file type.
70    pub file_type: FileType,
71    /// Bit 4 is unused, but we store it anyway so we can reproduce this field
72    /// verbatim.
73    pub unused_bit: bool,
74    /// Bit 5 is the "save with replace" flag.
75    pub save_with_replace_flag: bool,
76    /// Bit 6 is the "locked" flag, indicated by a ">" in directory listings.
77    pub locked_flag: bool,
78    /// Bit 7 is the "closed" flag.  Files are normally closed, so this bit is
79    /// normally set. Unclosed files are indicated in directory listings
80    /// with a "*", leading to such files being known as "splat files".
81    pub closed_flag: bool,
82}
83
84impl FileAttributes {
85    /// Parse a byte into a `FileAttributes` struct.
86    pub fn from_byte(byte: u8) -> FileAttributes {
87        let file_type = match byte & FILE_ATTRIB_FILE_TYPE_MASK {
88            FILE_TYPE_DEL => FileType::DEL,
89            FILE_TYPE_SEQ => FileType::SEQ,
90            FILE_TYPE_PRG => FileType::PRG,
91            FILE_TYPE_USR => FileType::USR,
92            FILE_TYPE_REL => FileType::REL,
93            b => FileType::Unknown(b),
94        };
95        FileAttributes {
96            file_type,
97            unused_bit: byte & FILE_ATTRIB_UNUSED_MASK != 0,
98            save_with_replace_flag: byte & FILE_ATTRIB_SAVE_WITH_REPLACE_MASK != 0,
99            locked_flag: byte & FILE_ATTRIB_LOCKED_MASK != 0,
100            closed_flag: byte & FILE_ATTRIB_CLOSED_MASK != 0,
101        }
102    }
103
104    /// Generate the byte which encodes this `FileAttributes` struct.
105    pub fn to_byte(&self) -> u8 {
106        let mut byte = match &self.file_type {
107            &FileType::DEL => FILE_TYPE_DEL,
108            &FileType::SEQ => FILE_TYPE_SEQ,
109            &FileType::PRG => FILE_TYPE_PRG,
110            &FileType::USR => FILE_TYPE_USR,
111            &FileType::REL => FILE_TYPE_REL,
112            &FileType::Unknown(b) => b,
113        };
114        if self.unused_bit {
115            byte = byte | FILE_ATTRIB_UNUSED_MASK
116        };
117        if self.save_with_replace_flag {
118            byte = byte | FILE_ATTRIB_SAVE_WITH_REPLACE_MASK
119        };
120        if self.locked_flag {
121            byte = byte | FILE_ATTRIB_LOCKED_MASK
122        };
123        if self.closed_flag {
124            byte = byte | FILE_ATTRIB_CLOSED_MASK
125        };
126        byte
127    }
128
129    /// Return true if this entry represents a properly deleted ("scratched")
130    /// file.  That is, if its file type is DEL and the closed flag is not
131    /// set.
132    pub fn is_scratched(&self) -> bool {
133        self.file_type == FileType::DEL && !self.closed_flag
134    }
135}
136
137impl fmt::Display for FileAttributes {
138    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
139        write!(
140            f,
141            "{}{}{}",
142            if self.closed_flag { ' ' } else { '*' },
143            self.file_type,
144            match (self.locked_flag, self.save_with_replace_flag) {
145                (true, false) => "<",
146                (false, true) => "@",
147                (true, true) => "<@",
148                (false, false) => " ",
149            },
150        )
151    }
152}
153
154impl fmt::Debug for FileAttributes {
155    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
156        // This is different from the Display impl in that there is no padding.
157        if !self.closed_flag {
158            f.write_char('*')?;
159        }
160        <FileType as fmt::Debug>::fmt(&self.file_type, f)?;
161        f.write_str(match (self.locked_flag, self.save_with_replace_flag) {
162            (true, false) => "<",
163            (false, true) => "@",
164            (true, true) => "<@",
165            (false, false) => "",
166        })
167    }
168}
169
170pub(super) const ENTRY_SIZE: usize = 32;
171const ENTRY_FILE_ATTRIBUTE_OFFSET: usize = 0x02;
172const ENTRY_FIRST_SECTOR_OFFSET: usize = 0x03;
173const ENTRY_FILENAME_OFFSET: usize = 0x05;
174const ENTRY_FILENAME_LENGTH: usize = 16;
175const ENTRY_EXTRA_OFFSET: usize = 0x15;
176const EXTRA_SIZE: usize = 9;
177const ENTRY_FILE_SIZE_OFFSET: usize = 0x1E;
178
179/// Different file storage schemes use the nine directory entry bytes
180/// 0x15..0x1E differently, hence the need for this enum to encapsulate the
181/// different interpretations.
182///
183/// NOTE: Technically, the two bytes @ 0x1C..0x1E are used in "save and
184/// replace" operations.  Maybe they should be parsed in some cases?  In
185/// regular files, they should normally be 0x00 unless a save-and-replace
186/// operation is currently in progress.  They are used to store the temporary
187/// next track/sector as the replacement data is written.
188#[derive(Clone, PartialEq)]
189pub enum Extra {
190    Linear(LinearExtra),
191    Relative(RelativeExtra),
192    GEOS(GEOSExtra),
193}
194
195impl Extra {
196    pub fn default() -> Extra {
197        Extra::Linear(LinearExtra::from_bytes(&[0u8; EXTRA_SIZE]))
198    }
199
200    pub fn from_bytes(scheme: Scheme, bytes: &[u8]) -> Extra {
201        assert_eq!(bytes.len(), EXTRA_SIZE);
202        match scheme {
203            Scheme::Linear => Extra::Linear(LinearExtra::from_bytes(bytes)),
204            Scheme::Relative => Extra::Relative(RelativeExtra::from_bytes(bytes)),
205            Scheme::GEOSSequential | Scheme::GEOSVLIR => Extra::GEOS(GEOSExtra::from_bytes(bytes)),
206        }
207    }
208
209    pub fn to_bytes(&self, bytes: &mut [u8]) {
210        assert_eq!(bytes.len(), EXTRA_SIZE);
211        match self {
212            Extra::Linear(e) => e.to_bytes(bytes),
213            Extra::Relative(e) => e.to_bytes(bytes),
214            Extra::GEOS(e) => e.to_bytes(bytes),
215        }
216    }
217}
218
219impl fmt::Debug for Extra {
220    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221        match self {
222            Extra::Linear(e) => e.fmt(f),
223            Extra::Relative(e) => e.fmt(f),
224            Extra::GEOS(e) => e.fmt(f),
225        }
226    }
227}
228
229/// The extra directory entry bytes used in regular files.  These should all be
230/// unused, so we simply preserve whatever bytes are present.
231#[derive(Clone, PartialEq)]
232pub struct LinearExtra {
233    pub unused: Vec<u8>, // 9 bytes
234}
235
236impl LinearExtra {
237    pub fn from_bytes(bytes: &[u8]) -> LinearExtra {
238        assert_eq!(bytes.len(), EXTRA_SIZE);
239        LinearExtra {
240            unused: bytes.to_vec(),
241        }
242    }
243
244    pub fn to_bytes(&self, bytes: &mut [u8]) {
245        assert_eq!(bytes.len(), EXTRA_SIZE);
246        (&mut bytes[..]).copy_from_slice(&self.unused);
247    }
248}
249
250impl fmt::Debug for LinearExtra {
251    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
252        write!(f, "REGULAR")
253    }
254}
255
256/// The extra directory entry bytes used in relative files, such as the record
257/// length and the location of the first side sector.
258#[derive(Clone, PartialEq)]
259pub struct RelativeExtra {
260    pub first_side_sector: Location,
261    pub record_length: u8,
262    pub unused: Vec<u8>, // 6 bytes
263}
264
265impl RelativeExtra {
266    const FIRST_SIDE_SECTOR_OFFSET: usize = 0x00;
267    const RECORD_LENGTH_OFFSET: usize = 0x02;
268    const UNUSED_OFFSET: usize = 0x03;
269
270    pub fn from_bytes(bytes: &[u8]) -> RelativeExtra {
271        assert_eq!(bytes.len(), EXTRA_SIZE);
272        RelativeExtra {
273            first_side_sector: Location::from_bytes(
274                &bytes[Self::FIRST_SIDE_SECTOR_OFFSET..Self::FIRST_SIDE_SECTOR_OFFSET + 2],
275            ),
276            record_length: bytes[Self::RECORD_LENGTH_OFFSET],
277            unused: bytes[Self::UNUSED_OFFSET..EXTRA_SIZE].to_vec(),
278        }
279    }
280
281    pub fn to_bytes(&self, bytes: &mut [u8]) {
282        assert_eq!(bytes.len(), EXTRA_SIZE);
283        bytes[Self::FIRST_SIDE_SECTOR_OFFSET] = self.first_side_sector.0;
284        bytes[Self::FIRST_SIDE_SECTOR_OFFSET + 1] = self.first_side_sector.1;
285        (&mut bytes[Self::UNUSED_OFFSET..EXTRA_SIZE]).copy_from_slice(&self.unused);
286    }
287}
288
289impl fmt::Debug for RelativeExtra {
290    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
291        write!(
292            f,
293            "REL(side={} rec_len={})",
294            self.first_side_sector, self.record_length
295        )
296    }
297}
298
299/// The extra directory entry bytes used in GEOS files.
300#[derive(Clone, PartialEq)]
301pub struct GEOSExtra {
302    pub info_block: Location,
303    pub structure: GEOSFileStructure,
304    pub geos_file_type: GEOSFileType,
305    pub year: u8,
306    pub month: u8,
307    pub day: u8,
308    pub hour: u8,
309    pub minute: u8,
310}
311
312impl GEOSExtra {
313    const INFO_BLOCK_OFFSET: usize = 0x00;
314    const STRUCTURE_OFFSET: usize = 0x02;
315    const GEOS_FILE_TYPE_OFFSET: usize = 0x03;
316    const YEAR_OFFSET: usize = 0x04;
317    const MONTH_OFFSET: usize = 0x05;
318    const DAY_OFFSET: usize = 0x06;
319    const HOUR_OFFSET: usize = 0x07;
320    const MINUTE_OFFSET: usize = 0x08;
321
322    pub fn from_bytes(bytes: &[u8]) -> GEOSExtra {
323        assert_eq!(bytes.len(), EXTRA_SIZE);
324        GEOSExtra {
325            info_block: Location::from_bytes(
326                &bytes[Self::INFO_BLOCK_OFFSET..Self::INFO_BLOCK_OFFSET + 2],
327            ),
328            structure: GEOSFileStructure::from_byte(bytes[Self::STRUCTURE_OFFSET]),
329            geos_file_type: GEOSFileType::from_byte(bytes[Self::GEOS_FILE_TYPE_OFFSET]),
330            year: bytes[Self::YEAR_OFFSET],
331            month: bytes[Self::MONTH_OFFSET],
332            day: bytes[Self::DAY_OFFSET],
333            hour: bytes[Self::HOUR_OFFSET],
334            minute: bytes[Self::MINUTE_OFFSET],
335        }
336    }
337
338    pub fn to_bytes(&self, bytes: &mut [u8]) {
339        assert_eq!(bytes.len(), EXTRA_SIZE);
340        self.info_block
341            .to_bytes(&mut bytes[Self::INFO_BLOCK_OFFSET..]);
342        bytes[Self::STRUCTURE_OFFSET] = self.structure.to_byte();
343        bytes[Self::GEOS_FILE_TYPE_OFFSET] = self.geos_file_type.to_byte();
344        bytes[Self::YEAR_OFFSET] = self.year;
345        bytes[Self::MONTH_OFFSET] = self.month;
346        bytes[Self::DAY_OFFSET] = self.day;
347        bytes[Self::HOUR_OFFSET] = self.hour;
348        bytes[Self::MINUTE_OFFSET] = self.minute;
349    }
350
351    #[inline]
352    fn is_entry_geos(bytes: &[u8]) -> bool {
353        // GEOS file detection works according to the instructions in GEOS.TXT, except
354        // for the following errata:
355        //     Check the bottom 3 bits of the D64 file type (byte position $02  of  the
356        //     directory entry). If it is not 0, 1 or 2  (but  3  or  higher,  REL  and
357        //     above), the file cannot be GEOS.
358        // It should read "If it is not 0, 1, 2 or 3  (but  4  or  higher,  REL and
359        // above), the file cannot be GEOS."
360        (bytes[ENTRY_FILE_ATTRIBUTE_OFFSET] & 0x07) < 4
361            && (bytes[ENTRY_EXTRA_OFFSET + Self::STRUCTURE_OFFSET] != 0
362                || bytes[ENTRY_EXTRA_OFFSET + Self::GEOS_FILE_TYPE_OFFSET] != 0)
363            && bytes[ENTRY_EXTRA_OFFSET + Self::STRUCTURE_OFFSET] <= 1
364    }
365
366    #[inline]
367    fn is_entry_vlir(bytes: &[u8]) -> bool {
368        bytes[ENTRY_EXTRA_OFFSET + Self::STRUCTURE_OFFSET] == 1
369    }
370}
371
372impl fmt::Debug for GEOSExtra {
373    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
374        write!(
375            f,
376            "GEOS({}, {}, {:04}-{:02}-{:02}-{:02}:{:02})",
377            self.structure,
378            self.geos_file_type,
379            1900 + (self.year as usize),
380            self.month,
381            self.day,
382            self.hour,
383            self.minute
384        )
385    }
386}
387
388/// A CBM DOS directory entry.
389#[derive(Clone)]
390pub struct DirectoryEntry {
391    pub file_attributes: FileAttributes,
392    pub first_sector: Location,
393    pub filename: Petscii,
394    pub extra: Extra,
395    pub file_size: u16,
396    // The disk image position where this entry is stored, if available.
397    pub position: Option<Position>,
398    pub scheme: Scheme,
399    // We'll keep non-CBM metadata private for now, so we can more easily refactor later.
400    geos_border: bool,
401}
402
403impl DirectoryEntry {
404    #[allow(unused)]
405    fn from_bytes(bytes: &[u8]) -> DirectoryEntry {
406        Self::parse(bytes, None)
407    }
408
409    fn from_positioned_bytes(bytes: &[u8], position: Position) -> DirectoryEntry {
410        Self::parse(bytes, Some(position))
411    }
412
413    fn parse(bytes: &[u8], position: Option<Position>) -> DirectoryEntry {
414        assert_eq!(bytes.len(), ENTRY_SIZE);
415
416        let file_attributes = FileAttributes::from_byte(bytes[ENTRY_FILE_ATTRIBUTE_OFFSET]);
417
418        // Determine the file storage scheme
419        let scheme = if file_attributes.file_type == FileType::REL {
420            Scheme::Relative
421        } else if GEOSExtra::is_entry_geos(&bytes) {
422            if GEOSExtra::is_entry_vlir(&bytes) {
423                Scheme::GEOSVLIR
424            } else {
425                Scheme::GEOSSequential
426            }
427        } else {
428            Scheme::Linear
429        };
430
431        // Parse extra metadata
432        let extra = Extra::from_bytes(
433            scheme,
434            &bytes[ENTRY_EXTRA_OFFSET..ENTRY_EXTRA_OFFSET + EXTRA_SIZE],
435        );
436
437        DirectoryEntry {
438            file_attributes,
439            first_sector: Location::from_bytes(&bytes[ENTRY_FIRST_SECTOR_OFFSET..]),
440            filename: Petscii::from_padded_bytes(
441                &bytes[ENTRY_FILENAME_OFFSET..ENTRY_FILENAME_OFFSET + ENTRY_FILENAME_LENGTH],
442                PADDING_BYTE,
443            ),
444            extra,
445            file_size: ((bytes[ENTRY_FILE_SIZE_OFFSET + 1] as u16) << 8)
446                | (bytes[ENTRY_FILE_SIZE_OFFSET] as u16),
447            position,
448            scheme,
449            geos_border: false,
450        }
451    }
452
453    /// Reset all fields to default values, in preparation for a fresh entry.
454    pub(super) fn reset(&mut self) {
455        self.file_attributes = FileAttributes::from_byte(0);
456        self.first_sector = Location::new(0, 0);
457        self.filename = Petscii::from_bytes(&[0u8; ENTRY_FILENAME_LENGTH]);
458        self.extra = Extra::default();
459        self.file_size = 0;
460        // The position field must be left untouched.
461    }
462
463    /// Read the serialized directory entry from the provided byte slice, and
464    /// update our fields. This is useful when reading an updated version
465    /// of the same entry.
466    fn reread_from_bytes(&mut self, bytes: &[u8]) {
467        let mut entry = DirectoryEntry::parse(bytes, self.position);
468        ::std::mem::swap(self, &mut entry);
469    }
470
471    /// Write the serialized directory entry to the provided mutable byte
472    /// slice.  This operation preserves any existing "next directory sector"
473    /// field.
474    pub fn to_bytes(&self, bytes: &mut [u8]) {
475        assert_eq!(bytes.len(), ENTRY_SIZE);
476        bytes[ENTRY_FILE_ATTRIBUTE_OFFSET] = self.file_attributes.to_byte();
477        bytes[ENTRY_FIRST_SECTOR_OFFSET] = self.first_sector.0;
478        bytes[ENTRY_FIRST_SECTOR_OFFSET + 1] = self.first_sector.1;
479        self.filename
480            .write_bytes_with_padding(
481                &mut bytes[ENTRY_FILENAME_OFFSET..ENTRY_FILENAME_OFFSET + ENTRY_FILENAME_LENGTH],
482                PADDING_BYTE,
483            )
484            .unwrap();
485        self.extra
486            .to_bytes(&mut bytes[ENTRY_EXTRA_OFFSET..ENTRY_EXTRA_OFFSET + EXTRA_SIZE]);
487        bytes[ENTRY_FILE_SIZE_OFFSET] = (self.file_size & 0xFF) as u8;
488        bytes[ENTRY_FILE_SIZE_OFFSET + 1] = (self.file_size >> 8) as u8;
489    }
490
491    /// Return true if this entry was found on the GEOS border block
492    pub fn is_geos_border(&self) -> bool {
493        self.geos_border
494    }
495}
496
497impl PositionedData for DirectoryEntry {
498    fn position(&self) -> io::Result<Position> {
499        match self.position {
500            Some(p) => Ok(p),
501            None => Err(DiskError::Unpositioned.into()),
502        }
503    }
504
505    fn positioned_read(&mut self, buffer: &[u8]) -> io::Result<()> {
506        let position = self.position()?;
507        if buffer.len() < position.size as usize {
508            return Err(DiskError::ReadUnderrun.into());
509        }
510        self.reread_from_bytes(buffer);
511        Ok(())
512    }
513
514    fn positioned_write(&self, buffer: &mut [u8]) -> io::Result<()> {
515        let position = self.position()?;
516        if buffer.len() < position.size as usize {
517            return Err(DiskError::WriteUnderrun.into());
518        }
519        self.to_bytes(buffer);
520        Ok(())
521    }
522}
523
524impl fmt::Display for DirectoryEntry {
525    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
526        write!(
527            f,
528            "{:<4} {:18}{}",
529            self.file_size,
530            format!("\"{}\"", self.filename),
531            self.file_attributes
532        )?;
533        if f.alternate() {
534            // verbose
535            write!(f, " {:?}", self.extra)?;
536        }
537        if self.geos_border {
538            write!(f, " (GEOS border)")?;
539        }
540        Ok(())
541    }
542}
543
544impl fmt::Debug for DirectoryEntry {
545    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
546        write!(
547            f,
548            "{},{},{:?} @ {:?}",
549            format!("\"{}\"", self.filename),
550            self.file_size,
551            self.file_attributes,
552            self.position
553        )
554    }
555}
556
557/// We use a boxed type for this instead of just a ChainIterator, so we can add
558/// the GEOS border block to the iteration if needed.
559type DirectoryBlockIterator = Box<Iterator<Item = io::Result<ChainSector>>>;
560
561/// This iterator will process the entire directory of a disk image and return
562/// a sequence of entries.
563pub struct DirectoryIterator {
564    block_iter: DirectoryBlockIterator,
565    chunks: ::std::vec::IntoIter<Vec<u8>>,
566    position: Position,
567    error: Option<io::Error>,
568    geos_border: Option<Location>,
569    processing_geos_border: bool,
570}
571
572impl DirectoryIterator {
573    /// Create a new directory iterator for the provided disk.
574    pub fn new<T: Disk>(disk: &T) -> DirectoryIterator
575    where
576        T: ?Sized,
577    {
578        let format = match disk.disk_format() {
579            Ok(f) => f,
580            Err(e) => return Self::new_error(disk, e),
581        };
582        let header = match disk.header() {
583            Ok(h) => h,
584            Err(e) => return Self::new_error(disk, e),
585        };
586        let location = format.first_directory_location();
587
588        // Prepare to iterate over the chain of directory blocks.
589        let chain = ChainIterator::new(disk.blocks(), location);
590
591        // If a GEOS border block is available, add it to the iteration at the end.
592        let (block_iter, geos_border): (DirectoryBlockIterator, Option<Location>) =
593            match header.geos {
594                Some(ref geos_header) => {
595                    let border_block_result = disk.blocks_ref().sector_owned(geos_header.border);
596                    let tail = iter::once(border_block_result.map(|block| ChainSector {
597                        data: block,
598                        location: geos_header.border,
599                    }));
600                    (Box::new(chain.chain(tail)), Some(geos_header.border))
601                }
602                None => (Box::new(chain), None),
603            };
604
605        DirectoryIterator {
606            block_iter,
607            chunks: vec![].into_iter(), // Arrange to return None the first time.
608            position: Position {
609                location: location,
610                offset: 0,
611                size: ENTRY_SIZE as u8,
612            },
613            error: None,
614            geos_border,
615            processing_geos_border: false,
616        }
617    }
618
619    /// We avoid returning an error in new(), so we can preserve the iter()
620    /// convention of returning an iterator directly.  Instead, we use this
621    /// method to produce an iterator that yields an error on the first
622    /// iteration.
623    fn new_error<T: Disk>(_disk: &T, error: io::Error) -> DirectoryIterator
624    where
625        T: ?Sized,
626    {
627        DirectoryIterator {
628            block_iter: Box::new(iter::empty()),
629            chunks: vec![].into_iter(),
630            position: Position {
631                location: Location(0, 0),
632                offset: 0,
633                size: ENTRY_SIZE as u8,
634            },
635            error: Some(error),
636            geos_border: None,
637            processing_geos_border: false,
638        }
639    }
640}
641
642impl Iterator for DirectoryIterator {
643    type Item = io::Result<DirectoryEntry>;
644
645    fn next(&mut self) -> Option<io::Result<DirectoryEntry>> {
646        // Return any pending error, if present.
647        let error = self.error.take();
648        if let Some(e) = error {
649            return Some(Err(e));
650        }
651
652        loop {
653            match self.chunks.next() {
654                Some(chunk) => {
655                    // Use exact_chunks() when it becomes stable, to avoid this check.
656                    if chunk.len() != ENTRY_SIZE {
657                        continue;
658                    }
659
660                    // Track the position of this entry.
661                    let entry_position = self.position;
662                    // The offset will normally wrap back to 0x00 when processing the last entry in
663                    // a sector.
664                    self.position.offset = self.position.offset.wrapping_add(ENTRY_SIZE as u8);
665
666                    // Parse
667                    let mut entry = DirectoryEntry::from_positioned_bytes(&chunk, entry_position);
668                    entry.geos_border = self.processing_geos_border;
669                    // Don't include scratched files
670                    if entry.file_attributes.is_scratched() {
671                        continue;
672                    }
673                    return Some(Ok(entry));
674                }
675                None => {
676                    match self.block_iter.next() {
677                        Some(Ok(block)) => {
678                            if let Some(border) = self.geos_border {
679                                self.processing_geos_border = border == block.location;
680                            }
681                            // Convert this block into a vector of entry-size chunk vectors.
682                            let mut chunks: Vec<Vec<u8>> = vec![];
683                            for chunk in block.data.chunks(ENTRY_SIZE) {
684                                chunks.push(chunk.to_vec());
685                            }
686                            self.chunks = chunks.into_iter();
687
688                            self.position.location = block.location;
689                            self.position.offset = 0;
690                            // Loop back to the Some(_) case to process the first chunk.
691                        }
692                        Some(Err(e)) => {
693                            return Some(Err(e));
694                        }
695                        None => {
696                            return None;
697                        }
698                    }
699                }
700            }
701        }
702    }
703}
704
705/// Return a `DirectoryEntry` representing the next free slot on the directory
706/// track.  This entry may be used to create a new directory entry by
707/// populating its fields and passing it to `Disk::write_directory_entry()`.
708pub(super) fn next_free_directory_entry<T: Disk>(disk: &mut T) -> io::Result<DirectoryEntry>
709where
710    T: ?Sized,
711{
712    let first_sector = disk.disk_format()?.first_directory_location();
713    let mut last_sector: Location = first_sector;
714
715    // Search the existing directory chain for a free slot.
716    {
717        let chain = ChainIterator::new(disk.blocks(), first_sector);
718        for chain_block in chain {
719            let ChainSector { data, location } = chain_block?;
720            last_sector = location;
721            let mut offset: u8 = 0;
722            for chunk in data.chunks(ENTRY_SIZE) {
723                let entry = DirectoryEntry::from_positioned_bytes(
724                    chunk,
725                    Position {
726                        location,
727                        offset,
728                        size: ENTRY_SIZE as u8,
729                    },
730                );
731                if entry.file_attributes.is_scratched() {
732                    return Ok(entry);
733                }
734                // The offset will normally wrap back to 0x00 when processing the last entry in
735                // a sector.
736                offset = offset.wrapping_add(ENTRY_SIZE as u8);
737            }
738        }
739    }
740
741    // No free slots are available in the currently allocated directory sectors, so
742    // we need to create a new one and link to it from the last found sector.
743    let bam = disk.bam()?;
744    let new_sector;
745    {
746        let bam = bam.borrow();
747        new_sector = bam.next_free_block(Some(last_sector))?;
748    }
749
750    // Write the new directory sector
751    let new_entry;
752    {
753        let mut blocks = disk.blocks_ref_mut();
754        let block = blocks.sector_mut(new_sector)?;
755        // The next location link will be (0x00,0xFF) to indicate that this is the last
756        // in the chain, and it is used in its entirety.
757        block[0] = 0x00;
758        block[1] = 0xFF;
759        // All other bytes should be zeroed.
760        for offset in 2..BLOCK_SIZE {
761            block[offset] = 0;
762        }
763        new_entry = DirectoryEntry::from_positioned_bytes(
764            &block[0..ENTRY_SIZE],
765            Position {
766                location: new_sector,
767                offset: 0,
768                size: ENTRY_SIZE as u8,
769            },
770        );
771    }
772
773    // Allocate the new sector in BAM
774    {
775        let mut bam = bam.borrow_mut();
776        bam.allocate(new_sector)?;
777    }
778
779    // Link to the new sector from the old sector
780    {
781        let mut blocks = disk.blocks_ref_mut();
782        let block = blocks.sector_mut(last_sector)?;
783        block[0] = new_sector.0;
784        block[1] = new_sector.1;
785    }
786
787    // Return the new entry
788    Ok(new_entry)
789}
790
791/// Confirm that the specified filename is valid.  A filename is considered
792/// valid if it is 16 characters or fewer.
793pub(super) fn check_filename_validity(filename: &Petscii) -> io::Result<()> {
794    if filename.len() > ENTRY_FILENAME_LENGTH {
795        return Err(DiskError::FilenameTooLong.into());
796    }
797    Ok(())
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803    use disk::d64::D64;
804    use disk::DiskFormat;
805
806    fn get_fresh_d64() -> (DiskFormat, D64) {
807        let mut d64 = D64::open_memory(D64::geometry(false)).unwrap();
808        d64.write_format(&"test".into(), &"t1".into()).unwrap();
809        let format = d64.disk_format().unwrap().clone();
810        (format, d64)
811    }
812
813    #[test]
814    fn test_next_free_directory_entry() {
815        const MAX_NEW_ENTRIES: usize = 1000;
816        const MAX_DIRECTORY_ENTRIES: usize = 144;
817        let (_format, mut disk) = get_fresh_d64();
818        let mut disk_full: bool = false;
819        let mut entries_written: usize = 0;
820        for _ in 0..MAX_NEW_ENTRIES {
821            let mut entry = match next_free_directory_entry(&mut disk) {
822                Ok(entry) => entry,
823                Err(ref e) => match DiskError::from_io_error(&e) {
824                    Some(ref e) if *e == DiskError::DiskFull => {
825                        disk_full = true;
826                        break;
827                    }
828                    Some(ref e) => panic!("error: {}", e),
829                    None => break,
830                },
831            };
832
833            entry.file_attributes.file_type = FileType::PRG;
834            entry.file_attributes.closed_flag = true;
835            entry.filename = "filename".into();
836            disk.write_directory_entry(&entry).unwrap();
837            entries_written += 1;
838        }
839        assert!(disk_full);
840        assert_eq!(entries_written, MAX_DIRECTORY_ENTRIES);
841    }
842
843    #[test]
844    fn test_directory_entry() {
845        // All bits cleared
846        static BUFFER1: [u8; ENTRY_SIZE] = [0u8; ENTRY_SIZE];
847        let entry = DirectoryEntry::from_bytes(&BUFFER1);
848        let mut output = [0u8; ENTRY_SIZE];
849        entry.to_bytes(&mut output);
850        assert_eq!(output, BUFFER1);
851        assert_eq!(entry.file_attributes.file_type, FileType::DEL);
852        assert_eq!(entry.file_attributes.unused_bit, false);
853        assert_eq!(entry.file_attributes.save_with_replace_flag, false);
854        assert_eq!(entry.file_attributes.locked_flag, false);
855        assert_eq!(entry.file_attributes.closed_flag, false);
856        assert_eq!(entry.first_sector, Location(0, 0));
857        assert_eq!(
858            entry.filename,
859            Petscii::from_bytes(&[0; ENTRY_FILENAME_LENGTH])
860        );
861        assert_eq!(entry.extra, Extra::default());
862        assert_eq!(entry.file_size, 0);
863
864        // All padding bytes
865        static BUFFER2: [u8; ENTRY_SIZE] = [PADDING_BYTE; ENTRY_SIZE];
866        let entry = DirectoryEntry::from_bytes(&BUFFER2);
867        let mut output = [0u8; ENTRY_SIZE];
868        output[0] = 0xa0; // to_bytes() doesn't touch the first two bytes
869        output[1] = 0xa0;
870        entry.to_bytes(&mut output);
871        assert_eq!(output, BUFFER2);
872        assert_eq!(entry.file_attributes.file_type, FileType::DEL);
873        assert_eq!(entry.file_attributes.unused_bit, false);
874        assert_eq!(entry.file_attributes.save_with_replace_flag, true);
875        assert_eq!(entry.file_attributes.locked_flag, false);
876        assert_eq!(entry.file_attributes.closed_flag, true);
877        assert_eq!(entry.first_sector, Location(PADDING_BYTE, PADDING_BYTE));
878        assert_eq!(entry.filename, Petscii::from_bytes(&[0; 0]));
879        assert_eq!(
880            entry.extra,
881            Extra::Linear(LinearExtra::from_bytes(&[PADDING_BYTE; EXTRA_SIZE]))
882        );
883        assert_eq!(
884            entry.file_size,
885            ((PADDING_BYTE as u16) << 8) | (PADDING_BYTE as u16)
886        );
887
888        // All bits set
889        static BUFFER3: [u8; ENTRY_SIZE] = [0xFFu8; ENTRY_SIZE];
890        let entry = DirectoryEntry::from_bytes(&BUFFER3);
891        let mut output = [0u8; ENTRY_SIZE];
892        output[0] = 0xff; // to_bytes() doesn't touch the first two bytes
893        output[1] = 0xff;
894        entry.to_bytes(&mut output);
895        assert_eq!(output, BUFFER3);
896        assert_eq!(entry.file_attributes.file_type, FileType::Unknown(0x0F));
897        assert_eq!(entry.file_attributes.unused_bit, true);
898        assert_eq!(entry.file_attributes.save_with_replace_flag, true);
899        assert_eq!(entry.file_attributes.locked_flag, true);
900        assert_eq!(entry.file_attributes.closed_flag, true);
901        assert_eq!(entry.first_sector, Location(0xFF, 0xFF));
902        assert_eq!(
903            entry.filename,
904            Petscii::from_bytes(&[0xFF; ENTRY_FILENAME_LENGTH])
905        );
906        assert_eq!(
907            entry.extra,
908            Extra::Linear(LinearExtra::from_bytes(&[0xFFu8; EXTRA_SIZE]))
909        );
910        assert_eq!(entry.file_size, 0xFFFF);
911
912        // A real world example.
913        // 00016620: 5347 8211 0541 5343 4949 2043 4f44 4553  SG...ASCII CODES
914        // 00016630: a0a0 a0a0 a000 0000 0000 0000 0000 0600  ................
915        // This is the second directory entry on the first directory sector.
916        // Why are the first two (presumably unused) bytes 0x53 0x47?  Who knows.
917        static BUFFER4: [u8; ENTRY_SIZE] = [
918            0x53, 0x47, 0x82, 0x11, 0x05, 0x41, 0x53, 0x43, 0x49, 0x49, 0x20, 0x43, 0x4f, 0x44,
919            0x45, 0x53, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
920            0x00, 0x00, 0x06, 0x00,
921        ];
922        let entry = DirectoryEntry::from_bytes(&BUFFER4);
923        let mut output = [0u8; ENTRY_SIZE];
924        output[0] = BUFFER4[0]; // to_bytes() doesn't touch the first two bytes
925        output[1] = BUFFER4[1];
926        entry.to_bytes(&mut output);
927        assert_eq!(output, BUFFER4);
928        assert_eq!(entry.file_attributes.file_type, FileType::PRG);
929        assert_eq!(entry.file_attributes.unused_bit, false);
930        assert_eq!(entry.file_attributes.save_with_replace_flag, false);
931        assert_eq!(entry.file_attributes.locked_flag, false);
932        assert_eq!(entry.file_attributes.closed_flag, true);
933        assert_eq!(entry.first_sector, Location(0x11, 0x05));
934        assert_eq!(entry.filename, Petscii::from_str("ascii codes"));
935        assert_eq!(entry.extra, Extra::default());
936        assert_eq!(entry.file_size, 0x0006);
937    }
938}