cpclib_files/
lib.rs

1use cpclib_common::camino::{Utf8Path, Utf8PathBuf};
2use cpclib_common::itertools::Itertools;
3use cpclib_disc::amsdos::{AmsdosAddBehavior, AmsdosError, AmsdosFile, AmsdosFileName};
4use cpclib_disc::disc::Disc;
5use cpclib_disc::edsk::Head;
6use cpclib_disc::open_disc;
7use either::Either;
8
9pub type AmsdosOrRaw<'d> = Either<AmsdosFile, &'d [u8]>;
10
11#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
12pub enum FileType {
13    AmsdosBin,
14    AmsdosBas,
15    Ascii,
16    NoHeader,
17    Auto
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub enum StorageSupport {
22    Disc(Utf8PathBuf),
23    Tape(Utf8PathBuf),
24    Host
25}
26
27impl StorageSupport {
28    pub fn in_disc(&self) -> bool {
29        matches!(self, Self::Disc(_))
30    }
31
32    pub fn in_tape(&self) -> bool {
33        matches!(self, Self::Tape(_))
34    }
35
36    pub fn in_host(&self) -> bool {
37        matches!(self, Self::Host)
38    }
39
40    pub fn container_filename(&self) -> Option<&Utf8Path> {
41        match self {
42            StorageSupport::Disc(d) => Some(d.as_path()),
43            StorageSupport::Tape(t) => Some(t.as_path()),
44            StorageSupport::Host => None
45        }
46    }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub struct FileAndSupport {
51    support: StorageSupport,
52    file: (FileType, Utf8PathBuf)
53}
54
55impl FileAndSupport {
56    delegate::delegate! {
57        to self.support {
58            pub fn in_disc(&self) -> bool;
59            pub fn in_tape(&self) -> bool;
60            pub fn in_host(&self) -> bool;
61            pub fn container_filename(&self) -> Option<&Utf8Path>;
62        }
63    }
64
65    pub fn new(support: StorageSupport, file: (FileType, Utf8PathBuf)) -> Self {
66        Self { support, file }
67    }
68
69    pub fn new_amsdos<P: Into<Utf8PathBuf>>(p: P) -> Self {
70        Self {
71            support: StorageSupport::Host,
72            file: (FileType::AmsdosBin, p.into())
73        }
74    }
75
76    pub fn new_amsdos_in_disc<P: Into<Utf8PathBuf>, F: Into<Utf8PathBuf>>(p: P, f: F) -> Self {
77        Self {
78            support: StorageSupport::Disc(p.into()),
79            file: (FileType::AmsdosBin, f.into())
80        }
81    }
82
83    pub fn new_basic<P: Into<Utf8PathBuf>>(p: P) -> Self {
84        Self {
85            support: StorageSupport::Host,
86            file: (FileType::AmsdosBas, p.into())
87        }
88    }
89
90    pub fn new_basic_in_disc<P: Into<Utf8PathBuf>, F: Into<Utf8PathBuf>>(p: P, f: F) -> Self {
91        Self {
92            support: StorageSupport::Disc(p.into()),
93            file: (FileType::AmsdosBas, f.into())
94        }
95    }
96
97    pub fn new_ascii<P: Into<Utf8PathBuf>>(p: P) -> Self {
98        Self {
99            support: StorageSupport::Host,
100            file: (FileType::Ascii, p.into())
101        }
102    }
103
104    pub fn new_ascii_in_disc<P: Into<Utf8PathBuf>, F: Into<Utf8PathBuf>>(p: P, f: F) -> Self {
105        Self {
106            support: StorageSupport::Disc(p.into()),
107            file: (FileType::Ascii, f.into())
108        }
109    }
110
111    pub fn new_no_header<P: Into<Utf8PathBuf>>(p: P) -> Self {
112        Self {
113            support: StorageSupport::Host,
114            file: (FileType::NoHeader, p.into())
115        }
116    }
117
118    pub fn new_auto<P: Into<Utf8PathBuf>>(p: P, header: bool) -> Self {
119        let fname = p.into();
120
121        const IMAGES_EXT: &[&str] = &[".dsk", ".edsk", ".hfe"];
122
123        let components = fname.as_str().split('#').collect_vec();
124        match components[..] {
125            [fname] => {
126                if header {
127                    Self::new_amsdos(fname)
128                }
129                else {
130                    Self::new_no_header(fname)
131                }
132            },
133            [first, second] => {
134                let is_image = IMAGES_EXT
135                    .iter()
136                    .any(|ext| first.to_ascii_lowercase().ends_with(ext));
137                if is_image {
138                    Self {
139                        support: StorageSupport::Disc(first.into()),
140                        file: (FileType::Auto, second.into())
141                    }
142                }
143                else if header {
144                    Self::new_amsdos(fname)
145                }
146                else {
147                    Self::new_no_header(fname)
148                }
149            },
150            _ => {
151                todo!("Need to handle case where fname as several #",)
152            }
153        }
154    }
155
156    pub fn filename(&self) -> Utf8PathBuf {
157        match &self.support {
158            StorageSupport::Disc(p) => Utf8PathBuf::from(format!("{}#{}", p, self.file.1)),
159            StorageSupport::Tape(utf8_path_buf) => todo!(),
160            StorageSupport::Host => Utf8PathBuf::from(format!("{}", &self.file.1))
161        }
162    }
163
164    pub fn amsdos_filename(&self) -> &Utf8Path {
165        &self.file.1
166    }
167
168    fn build_amsdos_bin_file(
169        &self,
170        data: &[u8],
171        loading_address: Option<u16>,
172        exec_address: Option<u16>
173    ) -> Result<AmsdosFile, AmsdosError> {
174        let size = data.len();
175        if size > 0x10000 {
176            return Err(AmsdosError::FileLargerThan64Kb);
177        }
178        let size = size as u16;
179
180        let loading_address = loading_address.unwrap_or(0);
181        let execution_address = exec_address
182            .map(|e| {
183                if e < loading_address + size {
184                    e
185                }
186                else {
187                    loading_address
188                }
189            })
190            .unwrap_or(loading_address);
191
192        AmsdosFile::binary_file_from_buffer(
193            &AmsdosFileName::try_from(self.amsdos_filename().as_str())?,
194            loading_address,
195            execution_address,
196            data
197        )
198    }
199
200    fn build_amsdos_bas_file(&self, data: &[u8]) -> Result<AmsdosFile, AmsdosError> {
201        AmsdosFile::basic_file_from_buffer(
202            &AmsdosFileName::try_from(self.amsdos_filename().as_str())?,
203            data
204        )
205    }
206
207    fn build_ascii_file(&self, data: &[u8]) -> Result<AmsdosFile, AmsdosError> {
208        match AmsdosFileName::try_from(self.amsdos_filename().as_str()) {
209            Ok(amsfname) => {
210                Ok(AmsdosFile::ascii_file_from_buffer_with_name(
211                    &amsfname, data
212                ))
213            },
214            Err(e) => {
215                if self.in_disc() {
216                    Err(e)?;
217                }
218                Ok(AmsdosFile::from_buffer(data))
219            }
220        }
221    }
222
223    pub fn build_file<'d>(
224        &self,
225        data: &'d [u8],
226        loading_address: Option<u16>,
227        exec_address: Option<u16>
228    ) -> Result<AmsdosOrRaw<'d>, AmsdosError> {
229        match self.resolve_file_type() {
230            FileType::AmsdosBin => {
231                self.build_amsdos_bin_file(data, loading_address, exec_address)
232                    .map(Either::Left)
233            },
234            FileType::AmsdosBas => self.build_amsdos_bas_file(data).map(Either::Left),
235            FileType::Ascii => self.build_ascii_file(data).map(Either::Left),
236            FileType::NoHeader => Ok(Either::Right(data)),
237            FileType::Auto => unreachable!()
238        }
239    }
240
241    pub fn save<D: AsRef<[u8]>>(
242        &self,
243        data: D,
244        loading_address: Option<u16>,
245        exec_address: Option<u16>,
246        add_behavior: Option<AmsdosAddBehavior>
247    ) -> Result<(), String> {
248        let data = data.as_ref();
249
250        let built_file = self
251            .build_file(data, loading_address, exec_address)
252            .map_err(|e| e.to_string())?;
253
254        match &self.support {
255            StorageSupport::Disc(disc_filename) => {
256                let mut disc =
257                    open_disc(disc_filename, false).map_err(|msg| format!("Disc error: {msg}"))?;
258
259                let head = Head::A;
260                let system = false;
261                let read_only = false;
262
263                let amsdos_file = built_file.unwrap_left();
264                disc.add_amsdos_file(
265                    &amsdos_file,
266                    head,
267                    read_only,
268                    system,
269                    add_behavior.unwrap_or(AmsdosAddBehavior::FailIfPresent)
270                )
271                .map_err(|e| e.to_string())?;
272
273                disc.save(disc_filename)
274                    .map_err(|e| format!("Error while saving {e}"))?;
275            },
276            StorageSupport::Tape(utf8_path_buf) => unimplemented!(),
277            StorageSupport::Host => {
278                // handle case with and without header
279                let (fname, content) = match &built_file {
280                    Either::Left(amsdos_file) => {
281                        if self.resolve_file_type() == FileType::Ascii {
282                            (self.filename().into(), amsdos_file.header_and_content())
283                        }
284                        else {
285                            let fname = amsdos_file
286                                .amsdos_filename()
287                                .unwrap()
288                                .unwrap()
289                                .ibm_filename();
290                            (fname, amsdos_file.header_and_content())
291                        }
292                    },
293                    Either::Right(buffer) => (self.filename().into(), *buffer)
294                };
295
296                std::fs::write(&fname, content)
297                    .map_err(|e| format!("Error while saving \"{fname}\". {e}"))?;
298            }
299        }
300
301        Ok(())
302    }
303
304    /// Ensure the file is not auto
305    pub fn resolve_file_type(&self) -> FileType {
306        match &self.file.0 {
307            FileType::Auto => {
308                let lower = self.amsdos_filename().as_str().to_lowercase();
309                if lower.ends_with(".bas") {
310                    FileType::AmsdosBas
311                }
312                else if lower.ends_with(".asc") {
313                    FileType::Ascii
314                }
315                else {
316                    FileType::AmsdosBin
317                }
318            },
319            other => *other
320        }
321    }
322}