d64lib/
lib.rs

1// SPDX-License-Identifier: MIT
2// Project: dtools
3// File: src/lib.rs
4// Author: Volker Schwaberow <volker@schwaberow.de>
5// Copyright (c) 2024 Volker Schwaberow
6
7use std::fs::File;
8use std::io::{Read, Seek, SeekFrom, Write};
9use thiserror::Error;
10
11#[cfg(test)]
12mod tests;
13
14const D64_35_TRACKS_SIZE: usize = 174848;
15const D64_40_TRACKS_SIZE: usize = 196608;
16const MAX_TRACKS: u8 = 40;
17const SECTORS_PER_TRACK: [u8; 40] = [
18    21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 19, 19, 19, 19, 19, 19, 19,
19    18, 18, 18, 18, 18, 18, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
20];
21
22#[derive(Error, Debug)]
23pub enum D64Error {
24    #[error("IO error: {0}")]
25    Io(#[from] std::io::Error),
26    #[error("Invalid D64 file size")]
27    InvalidFileSize,
28    #[error("Invalid track or sector")]
29    InvalidTrackSector,
30    #[error("File not found")]
31    FileNotFound,
32    #[error("Disk full")]
33    DiskFull,
34}
35
36pub struct D64 {
37    pub data: Vec<u8>,
38    pub tracks: u8,
39}
40
41pub struct BAM {
42    pub tracks: u8,
43    pub free_sectors: [u8; 40],
44    pub bitmap: [[u8; 3]; 40],
45    pub disk_name: [u8; 16],
46    pub disk_id: [u8; 2],
47    pub dos_type: u8,
48}
49
50pub fn petscii_to_ascii(petscii: &[u8]) -> String {
51    petscii
52        .iter()
53        .map(|&c| match c {
54            0x20..=0x5F => c as char,
55            0xC1..=0xDA => (c - 0x80) as char,
56            _ => '?',
57        })
58        .collect()
59}
60
61pub fn ascii_to_petscii(ascii: &str) -> Vec<u8> {
62    ascii
63        .chars()
64        .map(|c| match c {
65            ' '..='_' => c as u8,
66            'a'..='z' => (c as u8) - 32,
67            _ => 0x3F,
68        })
69        .collect()
70}
71
72impl D64 {
73    pub fn new(tracks: u8) -> Result<Self, D64Error> {
74        if tracks != 35 && tracks != 40 {
75            return Err(D64Error::InvalidFileSize);
76        }
77        let size = if tracks == 35 {
78            D64_35_TRACKS_SIZE
79        } else {
80            D64_40_TRACKS_SIZE
81        };
82        Ok(Self {
83            data: vec![0; size],
84            tracks,
85        })
86    }
87
88    pub fn format(&mut self, disk_name: &str, disk_id: &str) -> Result<(), D64Error> {
89        self.data.fill(0);
90
91        let mut bam = [0u8; 256];
92        bam[0] = 18;
93        bam[1] = 1;
94        bam[2] = 0x41;
95
96        for track in 1..=self.tracks {
97            let track_idx = (track - 1) as usize;
98            let sectors = SECTORS_PER_TRACK[track_idx];
99            bam[4 + track_idx * 4] = sectors;
100            bam[5 + track_idx * 4] = 0xFF;
101            bam[6 + track_idx * 4] = 0xFF;
102            bam[7 + track_idx * 4] = if sectors > 16 {
103                0xFF
104            } else {
105                (1 << sectors) - 1
106            };
107        }
108
109        for track in 18..=19 {
110            let track_idx = (track - 1) as usize;
111            bam[4 + track_idx * 4] = 0;
112            bam[5 + track_idx * 4] = 0;
113            bam[6 + track_idx * 4] = 0;
114            bam[7 + track_idx * 4] = 0;
115        }
116
117        let disk_name_bytes = ascii_to_petscii(disk_name);
118        let disk_id_bytes = ascii_to_petscii(disk_id);
119        bam[144..144 + disk_name_bytes.len()].copy_from_slice(&disk_name_bytes);
120        bam[162..164].copy_from_slice(&disk_id_bytes);
121
122        self.write_sector(18, 0, &bam)?;
123
124        let mut dir = [0u8; 256];
125        dir[1] = 0xFF;
126        self.write_sector(18, 1, &dir)?;
127
128        Ok(())
129    }
130
131    pub fn from_file(path: &str) -> Result<Self, D64Error> {
132        let mut file = File::open(path)?;
133        let mut data = Vec::new();
134        file.read_to_end(&mut data)?;
135
136        let tracks = match data.len() {
137            D64_35_TRACKS_SIZE => 35,
138            D64_40_TRACKS_SIZE => 40,
139            _ => return Err(D64Error::InvalidFileSize),
140        };
141
142        Ok(Self { data, tracks })
143    }
144
145    pub fn save_to_file(&self, path: &str) -> Result<(), D64Error> {
146        let mut file = File::create(path)?;
147        file.write_all(&self.data)?;
148        Ok(())
149    }
150
151    pub fn read_sector(&self, track: u8, sector: u8) -> Result<&[u8], D64Error> {
152        let offset = self.sector_offset(track, sector)?;
153        Ok(&self.data[offset..offset + 256])
154    }
155
156    pub fn write_sector(&mut self, track: u8, sector: u8, data: &[u8]) -> Result<(), D64Error> {
157        let offset = self.sector_offset(track, sector)?;
158        self.data[offset..offset + 256].copy_from_slice(data);
159        Ok(())
160    }
161
162    pub fn trace_file(&self, filename: &str) -> Result<Vec<(u8, u8)>, D64Error> {
163        let (start_track, start_sector) = self.find_file(filename)?;
164        let mut sectors = Vec::new();
165        let mut track = start_track;
166        let mut sector = start_sector;
167
168        loop {
169            sectors.push((track, sector));
170            let data = self.read_sector(track, sector)?;
171            let next_track = data[0];
172            let next_sector = data[1];
173
174            if next_track == 0 {
175                break;
176            }
177            track = next_track;
178            sector = next_sector;
179        }
180
181        Ok(sectors)
182    }
183
184    fn sector_offset(&self, track: u8, sector: u8) -> Result<usize, D64Error> {
185        if track == 0 || track > self.tracks || sector >= SECTORS_PER_TRACK[(track - 1) as usize] {
186            return Err(D64Error::InvalidTrackSector);
187        }
188
189        let mut offset = 0;
190        for t in 1..track {
191            offset += SECTORS_PER_TRACK[(t - 1) as usize] as usize * 256;
192        }
193        offset += sector as usize * 256;
194
195        Ok(offset)
196    }
197
198    pub fn list_files(&self) -> Result<Vec<String>, D64Error> {
199        let mut files = Vec::new();
200        let dir_track = 18;
201        let mut sector = 1;
202        let mut visited_sectors = std::collections::HashSet::new();
203
204        loop {
205            if visited_sectors.contains(&(dir_track, sector)) {
206                return Err(D64Error::InvalidTrackSector);
207            }
208            visited_sectors.insert((dir_track, sector));
209
210            let data = self.read_sector(dir_track, sector)?;
211
212            for i in (0..256).step_by(32) {
213                let file_type = data[i + 2];
214                if file_type == 0 {
215                    continue;
216                }
217                if file_type != 0 && file_type & 0x07 != 0 {
218                    let name_end = data[i + 5..i + 21]
219                        .iter()
220                        .position(|&x| x == 0xA0)
221                        .unwrap_or(16);
222                    let name = petscii_to_ascii(&data[i + 5..i + 5 + name_end]);
223                    files.push(name);
224                }
225            }
226
227            let next_track = data[0];
228            let next_sector = data[1];
229
230            if next_track == 0 || (next_track == 18 && next_sector == 1) {
231                break;
232            }
233
234            if next_track != 18 || next_sector >= SECTORS_PER_TRACK[17] {
235                return Err(D64Error::InvalidTrackSector);
236            }
237
238            sector = next_sector;
239        }
240
241        Ok(files)
242    }
243
244    pub fn extract_file(&self, filename: &str) -> Result<Vec<u8>, D64Error> {
245        let (start_track, start_sector) = self.find_file(filename)?;
246        let mut content = Vec::new();
247        let mut track = start_track;
248        let mut sector = start_sector;
249
250        loop {
251            let data = self.read_sector(track, sector)?;
252            let next_track = data[0];
253            let next_sector = data[1];
254            let bytes_to_read = if next_track == 0 { next_sector } else { 254 };
255            content.extend_from_slice(&data[2..2 + bytes_to_read as usize]);
256
257            if next_track == 0 {
258                break;
259            }
260            track = next_track;
261            sector = next_sector;
262        }
263
264        Ok(content)
265    }
266
267    pub fn insert_file(&mut self, filename: &str, content: &[u8]) -> Result<(), D64Error> {
268        let (mut track, mut sector) = self.find_free_sector()?;
269        let mut remaining = content;
270
271        let dir_entry = self.create_dir_entry(filename, track, sector)?;
272        self.write_dir_entry(dir_entry)?;
273
274        while !remaining.is_empty() {
275            let mut sector_data = vec![0; 256];
276            let (next_track, next_sector) = if remaining.len() > 254 {
277                sector_data[0] = track;
278                sector_data[1] = sector + 1;
279                if sector + 1 >= SECTORS_PER_TRACK[(track - 1) as usize] {
280                    (track + 1, 0)
281                } else {
282                    (track, sector + 1)
283                }
284            } else {
285                sector_data[0] = 0;
286                sector_data[1] = remaining.len() as u8;
287                (0, 0)
288            };
289
290            let bytes_to_write = remaining.len().min(254);
291            sector_data[2..2 + bytes_to_write].copy_from_slice(&remaining[..bytes_to_write]);
292            self.write_sector(track, sector, &sector_data)?;
293
294            remaining = &remaining[bytes_to_write..];
295            track = next_track;
296            sector = next_sector;
297
298            if track == 0 {
299                break;
300            }
301        }
302
303        Ok(())
304    }
305
306    fn find_file(&self, filename: &str) -> Result<(u8, u8), D64Error> {
307        let dir_track = 18;
308        let mut sector = 1;
309
310        loop {
311            let data = self.read_sector(dir_track, sector)?;
312            for i in (0..256).step_by(32) {
313                let file_type = data[i + 2];
314                if file_type != 0 && file_type & 0x07 != 0 {
315                    let name = petscii_to_ascii(&data[i + 5..i + 21]);
316                    if name.trim() == filename {
317                        return Ok((data[i + 3], data[i + 4]));
318                    }
319                }
320            }
321            sector = data[1];
322            if sector == 0 {
323                break;
324            }
325        }
326
327        Err(D64Error::FileNotFound)
328    }
329
330    pub fn read_bam(&self) -> Result<BAM, D64Error> {
331        let bam_data = self.read_sector(18, 0)?;
332        BAM::from_sector_data(bam_data, self.tracks)
333    }
334
335    pub fn write_bam(&mut self, bam: &BAM) -> Result<(), D64Error> {
336        let bam_data = bam.to_sector_data();
337        self.write_sector(18, 0, &bam_data)
338    }
339
340    pub fn allocate_sector(&mut self, track: u8, sector: u8) -> Result<(), D64Error> {
341        let mut bam = self.read_bam()?;
342        bam.allocate_sector(track, sector)?;
343        self.write_bam(&bam)
344    }
345
346    pub fn free_sector(&mut self, track: u8, sector: u8) -> Result<(), D64Error> {
347        let mut bam = self.read_bam()?;
348        bam.free_sector(track, sector)?;
349        self.write_bam(&bam)
350    }
351
352    pub fn find_free_sector(&self) -> Result<(u8, u8), D64Error> {
353        let bam = self.read_bam()?;
354        for track in 1..=self.tracks {
355            if let Some(sector) = bam.find_free_sector(track) {
356                return Ok((track, sector));
357            }
358        }
359        Err(D64Error::DiskFull)
360    }
361
362    fn create_dir_entry(
363        &self,
364        filename: &str,
365        track: u8,
366        sector: u8,
367    ) -> Result<[u8; 32], D64Error> {
368        let mut entry = [0u8; 32];
369        entry[2] = 0x82;
370        entry[3] = track;
371        entry[4] = sector;
372        let name_bytes = ascii_to_petscii(filename);
373        entry[5..5 + name_bytes.len()].copy_from_slice(&name_bytes);
374        Ok(entry)
375    }
376
377    fn write_dir_entry(&mut self, entry: [u8; 32]) -> Result<(), D64Error> {
378        let dir_track = 18;
379        let mut sector = 1;
380
381        loop {
382            let mut data = self.read_sector(dir_track, sector)?.to_vec();
383            for i in (0..256).step_by(32) {
384                if data[i + 2] == 0 {
385                    data[i..i + 32].copy_from_slice(&entry);
386                    self.write_sector(dir_track, sector, &data)?;
387                    return Ok(());
388                }
389            }
390            sector = data[1];
391            if sector == 0 {
392                return Err(D64Error::DiskFull);
393            }
394        }
395    }
396}
397
398impl BAM {
399    fn from_sector_data(data: &[u8], tracks: u8) -> Result<Self, D64Error> {
400        let mut bam = BAM {
401            tracks,
402            free_sectors: [0; 40],
403            bitmap: [[0; 3]; 40],
404            disk_name: [0; 16],
405            disk_id: [0; 2],
406            dos_type: data[2],
407        };
408
409        for track in 0..tracks as usize {
410            bam.free_sectors[track] = data[4 + track * 4];
411            bam.bitmap[track][0] = data[5 + track * 4];
412            bam.bitmap[track][1] = data[6 + track * 4];
413            bam.bitmap[track][2] = data[7 + track * 4];
414        }
415
416        bam.disk_name.copy_from_slice(&data[144..160]);
417        bam.disk_id.copy_from_slice(&data[162..164]);
418
419        Ok(bam)
420    }
421
422    fn to_sector_data(&self) -> Vec<u8> {
423        let mut data = vec![0; 256];
424        data[0] = 18;
425        data[1] = 1;
426        data[2] = self.dos_type;
427
428        for track in 0..self.tracks as usize {
429            data[4 + track * 4] = self.free_sectors[track];
430            data[5 + track * 4] = self.bitmap[track][0];
431            data[6 + track * 4] = self.bitmap[track][1];
432            data[7 + track * 4] = self.bitmap[track][2];
433        }
434
435        data[144..160].copy_from_slice(&self.disk_name);
436        data[162..164].copy_from_slice(&self.disk_id);
437
438        data
439    }
440
441    pub fn allocate_sector(&mut self, track: u8, sector: u8) -> Result<(), D64Error> {
442        if track == 0 || track > self.tracks || sector >= SECTORS_PER_TRACK[(track - 1) as usize] {
443            return Err(D64Error::InvalidTrackSector);
444        }
445
446        let track_idx = (track - 1) as usize;
447        let byte_idx = (sector / 8) as usize;
448        let bit_idx = sector % 8;
449
450        if self.bitmap[track_idx][byte_idx] & (1 << bit_idx) == 0 {
451            return Ok(());
452        }
453
454        self.bitmap[track_idx][byte_idx] &= !(1 << bit_idx);
455        self.free_sectors[track_idx] -= 1;
456
457        Ok(())
458    }
459
460    pub fn free_sector(&mut self, track: u8, sector: u8) -> Result<(), D64Error> {
461        if track == 0 || track > self.tracks || sector >= SECTORS_PER_TRACK[(track - 1) as usize] {
462            return Err(D64Error::InvalidTrackSector);
463        }
464
465        let track_idx = (track - 1) as usize;
466        let byte_idx = (sector / 8) as usize;
467        let bit_idx = sector % 8;
468
469        if self.bitmap[track_idx][byte_idx] & (1 << bit_idx) != 0 {
470            return Ok(());
471        }
472
473        self.bitmap[track_idx][byte_idx] |= 1 << bit_idx;
474        self.free_sectors[track_idx] += 1;
475
476        Ok(())
477    }
478
479    pub fn find_free_sector(&self, track: u8) -> Option<u8> {
480        if track == 0 || track > self.tracks {
481            return None;
482        }
483
484        let track_idx = (track - 1) as usize;
485        for (byte_idx, &byte) in self.bitmap[track_idx].iter().enumerate() {
486            if byte != 0 {
487                for bit_idx in 0..8 {
488                    if byte & (1 << bit_idx) != 0 {
489                        let sector = (byte_idx as u8) * 8 + bit_idx;
490                        if sector < SECTORS_PER_TRACK[track_idx] {
491                            return Some(sector);
492                        }
493                    }
494                }
495            }
496        }
497        None
498    }
499
500    pub fn get_free_sectors_count(&self, track: u8) -> Result<u8, D64Error> {
501        if track == 0 || track > self.tracks {
502            return Err(D64Error::InvalidTrackSector);
503        }
504        Ok(self.free_sectors[(track - 1) as usize])
505    }
506
507    pub fn get_disk_name(&self) -> String {
508        petscii_to_ascii(&self.disk_name)
509    }
510
511    pub fn get_disk_id(&self) -> String {
512        petscii_to_ascii(&self.disk_id)
513    }
514
515    pub fn set_disk_name(&mut self, name: &str) {
516        let name_bytes = ascii_to_petscii(name);
517        self.disk_name[..name_bytes.len()].copy_from_slice(&name_bytes);
518        self.disk_name[name_bytes.len()..].fill(0xA0);
519    }
520
521    pub fn set_disk_id(&mut self, id: &str) {
522        let id_bytes = ascii_to_petscii(id);
523        self.disk_id.copy_from_slice(&id_bytes[..2]);
524    }
525}