cpclib_asm/assembler/
file.rs

1use std::borrow::Cow;
2use std::collections::VecDeque;
3use std::fs::File;
4use std::io::Read;
5use std::ops::Deref;
6
7use cpclib_common::camino::{Utf8Path, Utf8PathBuf};
8use cpclib_common::itertools::Itertools;
9use cpclib_disc::amsdos::{AmsdosFileName, AmsdosHeader, AmsdosManagerNonMut};
10use cpclib_disc::disc::Disc;
11use cpclib_disc::edsk::{ExtendedDsk, Head};
12use either::Either;
13
14use super::Env;
15use super::embedded::EmbeddedFiles;
16use crate::error::AssemblerError;
17use crate::preamble::ParserOptions;
18use crate::progress::Progress;
19
20pub struct Fname<'a, 'b>(Either<&'a Utf8Path, (&'a str, &'b Env)>);
21
22impl<'a, 'b> Deref for Fname<'a, 'b> {
23    type Target = Either<&'a Utf8Path, (&'a str, &'b Env)>;
24
25    fn deref(&self) -> &Self::Target {
26        &self.0
27    }
28}
29
30impl<'a> From<&'a Utf8Path> for Fname<'a, '_> {
31    fn from(value: &'a Utf8Path) -> Self {
32        Self(Either::Left(value))
33    }
34}
35
36impl<'a> From<&'a str> for Fname<'a, '_> {
37    fn from(value: &'a str) -> Self {
38        let p: &Utf8Path = value.into();
39        p.into()
40    }
41}
42
43impl<'a, 'b> From<(&'a str, &'b Env)> for Fname<'a, 'b> {
44    fn from(value: (&'a str, &'b Env)) -> Self {
45        Self(Either::Right(value))
46    }
47}
48
49pub enum AnyFileNameOwned {
50    InImage { image: String, content: String },
51    Standard(String)
52}
53
54impl From<&AnyFileName<'_>> for AnyFileNameOwned {
55    fn from(value: &AnyFileName) -> Self {
56        match value {
57            AnyFileName::InImage { image, content } => Self::new_in_image(*image, *content),
58            AnyFileName::Standard(content) => Self::new_standard(*content)
59        }
60    }
61}
62
63impl<'fname> From<&'fname AnyFileNameOwned> for AnyFileName<'fname> {
64    fn from(value: &'fname AnyFileNameOwned) -> Self {
65        match value {
66            AnyFileNameOwned::InImage {
67                image,
68                content: amsdos
69            } => {
70                AnyFileName::InImage {
71                    image: image.as_str(),
72                    content: amsdos.as_str()
73                }
74            },
75            AnyFileNameOwned::Standard(fname) => AnyFileName::Standard(fname.as_str())
76        }
77    }
78}
79
80impl From<&str> for AnyFileNameOwned {
81    fn from(value: &str) -> Self {
82        let any = AnyFileName::from(value);
83        AnyFileNameOwned::from(&any)
84    }
85}
86
87impl AnyFileNameOwned {
88    pub fn new_standard<S: Into<String>>(fname: S) -> Self {
89        Self::Standard(fname.into())
90    }
91
92    pub fn new_in_image<S1: Into<String>, S2: Into<String>>(image: S1, amsdos: S2) -> Self {
93        Self::InImage {
94            image: image.into(),
95            content: amsdos.into()
96        }
97    }
98
99    pub fn as_any_filename(&self) -> AnyFileName {
100        AnyFileName::from(self)
101    }
102}
103
104/// Helper to handler filenames that contains both a dsk name and a file
105pub enum AnyFileName<'fname> {
106    InImage {
107        image: &'fname str,
108        content: &'fname str
109    },
110    Standard(&'fname str)
111}
112
113impl<'fname> AnyFileName<'fname> {
114    const DSK_SEPARATOR: char = '#';
115
116    pub fn new_standard(fname: &'fname str) -> Self {
117        Self::Standard(fname)
118    }
119
120    pub fn new_in_image(image: &'fname str, amsdos: &'fname str) -> Self {
121        Self::InImage {
122            image,
123            content: amsdos
124        }
125    }
126
127    pub fn use_image(&self) -> bool {
128        match self {
129            AnyFileName::InImage { .. } => true,
130            _ => false
131        }
132    }
133
134    pub fn image_filename(&self) -> Option<&str> {
135        match self {
136            AnyFileName::InImage { image, .. } => Some(image),
137            AnyFileName::Standard(_) => None
138        }
139    }
140
141    pub fn content_filename(&self) -> &str {
142        match self {
143            AnyFileName::InImage {
144                image,
145                content: amsdos
146            } => amsdos,
147            AnyFileName::Standard(content) => content
148        }
149    }
150
151    pub fn basm_fname(&self) -> Cow<str> {
152        match self {
153            AnyFileName::InImage { image, content } => {
154                Cow::Owned(format!("{}{}{}", image, Self::DSK_SEPARATOR, content))
155            },
156            AnyFileName::Standard(content) => Cow::Borrowed(content)
157        }
158    }
159
160    fn base_filename(&self) -> &str {
161        match self {
162            AnyFileName::InImage {
163                image,
164                content: amsdos
165            } => image,
166            AnyFileName::Standard(f) => f
167        }
168    }
169
170    pub fn path_for_base_filename(
171        &self,
172        options: &ParserOptions,
173        env: Option<&Env>
174    ) -> Result<Utf8PathBuf, AssemblerError> {
175        let real_fname = self.base_filename();
176
177        let res = options.get_path_for(real_fname, env).map_err(|e| {
178            match e {
179                either::Either::Left(asm) => asm,
180                either::Either::Right(tested) => {
181                    AssemblerError::AssemblingError {
182                        msg: format!("{} not found. Tested {:?}", self.base_filename(), tested)
183                    }
184                },
185            }
186        })?;
187
188        let res = if self.image_filename().is_some() {
189            let mut s = res.to_string();
190            s.push(Self::DSK_SEPARATOR);
191            s.push_str(self.content_filename());
192            Utf8PathBuf::from(&s)
193        }
194        else {
195            res
196        };
197
198        Ok(res)
199    }
200}
201
202impl<'fname> From<&'fname str> for AnyFileName<'fname> {
203    fn from(fname: &'fname str) -> Self {
204        const IMAGES_EXT: &[&str] = &[".dsk", ".edsk", ".hfe"];
205
206        let components = fname.split(Self::DSK_SEPARATOR).collect_vec();
207        match components[..] {
208            [fname] => AnyFileName::Standard(fname),
209            [first, second] => {
210                let is_image = IMAGES_EXT
211                    .iter()
212                    .any(|ext| first.to_ascii_lowercase().ends_with(ext));
213                if is_image {
214                    AnyFileName::InImage {
215                        image: first,
216                        content: second
217                    }
218                }
219                else {
220                    AnyFileName::Standard(fname)
221                }
222            },
223            _ => {
224                todo!(
225                    "Need to handle case where fname as several {}",
226                    Self::DSK_SEPARATOR
227                )
228            }
229        }
230    }
231}
232
233pub fn get_filename_to_read<S: AsRef<str>>(
234    fname: S,
235    options: &ParserOptions,
236    env: Option<&Env>
237) -> Result<Utf8PathBuf, AssemblerError> {
238    let fname = fname.as_ref();
239
240    AnyFileName::from(fname).path_for_base_filename(options, env)
241}
242
243/// TODO refactor and move that from asm stuff. Should be done only in the disc crate
244/// Load a file and remove header if any
245/// - if path is provided, this is the file name used
246/// - if a string is provided, there is a search of appropriate filename
247pub fn load_file<'a, 'b, F: Into<Fname<'a, 'b>>>(
248    fname: F,
249    options: &ParserOptions
250) -> Result<(VecDeque<u8>, Option<AmsdosHeader>), AssemblerError> {
251    let fname = fname.into();
252    let true_fname = match &fname.deref() {
253        either::Either::Right((p, env)) => get_filename_to_read(p, options, Some(env))?,
254        either::Either::Left(p) => p.into()
255    };
256
257    let true_name = true_fname.as_str();
258    let any_filename: AnyFileName<'_> = true_name.into();
259    let (data, header) = if !any_filename.use_image() {
260        // here we handle a standard file
261
262        // Get the file content
263        let data = load_file_raw(any_filename.content_filename(), options)?;
264        let mut data = VecDeque::from(data);
265
266        // get a slice on the data to ease its cut
267        let header = if data.len() >= 128 {
268            // by construction there is only one slice
269            let header = AmsdosHeader::from_buffer(data.as_slices().0);
270
271            // XXX previously, I was checking the file name validity, but it is a
272            //     bad heursitic as orgams does not respect that
273            if (header.file_length() + 128) as usize == data.len() {
274                data.drain(..128);
275                Some(header)
276            }
277            else {
278                None
279            }
280        }
281        else {
282            None
283        };
284
285        (data, header)
286    }
287    else {
288        // here we read from a dsk
289        let image_fname = any_filename.image_filename().unwrap();
290        let amsdos_fname = any_filename.content_filename();
291
292        let disc: Box<ExtendedDsk> /* we cannot use Disc ATM */ = if image_fname.to_ascii_uppercase().ends_with(".DSK") {
293            Box::new(ExtendedDsk::open(image_fname).map_err(|e| AssemblerError::AssemblingError { msg: e })?)
294
295        } else {
296            unimplemented!("Need to code loading of {image_fname}. Disc trait needs to be simplifed by removing all generic parameters :(");
297        };
298
299        let manager = AmsdosManagerNonMut::new_from_disc(&disc, Head::A);
300        let file = manager
301            .get_file(AmsdosFileName::try_from(amsdos_fname)?)
302            .ok_or_else(|| {
303                AssemblerError::AssemblingError {
304                    msg: format!("Unable to get {amsdos_fname}")
305                }
306            })?;
307
308        let header = file.header();
309        let data = VecDeque::from_iter(file.content().iter().cloned());
310
311        (data, header)
312    };
313
314    Ok((data, header))
315}
316
317/// Load a file and keep the header if any
318pub fn load_file_raw<'a, 'b, F: Into<Fname<'a, 'b>>>(
319    fname: F,
320    options: &ParserOptions
321) -> Result<Vec<u8>, AssemblerError> {
322    let fname = fname.into();
323
324    // Retreive fname
325    let fname = match &fname.deref() {
326        either::Either::Right((p, env)) => get_filename_to_read(p, options, Some(env))?,
327        either::Either::Left(p) => p.into()
328    };
329
330    let fname_repr = fname.as_str();
331
332    let progress = if options.show_progress {
333        Progress::progress().add_load(fname_repr);
334        Some(fname_repr)
335    }
336    else {
337        None
338    };
339
340    // Get the content from the inner files or the disc
341    let content = if fname_repr.starts_with("inner://") {
342        // handle inner file
343        EmbeddedFiles::get(fname_repr)
344            .ok_or(AssemblerError::IOError {
345                msg: format!("Unable to open {:?}; it is not embedded.", fname_repr)
346            })?
347            .data
348            .to_vec()
349    }
350    else {
351        // handle real file
352        let mut f = File::open(&fname).map_err(|e| {
353            AssemblerError::IOError {
354                msg: format!("Unable to open {:?}. {}", fname, e)
355            }
356        })?;
357
358        let mut content = Vec::new();
359        f.read_to_end(&mut content).map_err(|e| {
360            AssemblerError::IOError {
361                msg: format!("Unable to read {:?}. {}", fname, e)
362            }
363        })?;
364
365        content
366    };
367
368    if let Some(progress) = progress {
369        Progress::progress().remove_load(progress);
370    }
371    Ok(content)
372}
373
374/// Read the content of the source file.
375/// Uses the context to obtain the appropriate file other the included directories
376pub fn read_source<P: AsRef<Utf8Path>>(
377    fname: P,
378    options: &ParserOptions
379) -> Result<String, AssemblerError> {
380    let fname = fname.as_ref();
381
382    let (mut content, header_removed) = load_file(fname, options)?;
383    assert!(header_removed.is_none());
384
385    let content = content.make_contiguous();
386    // handle_source_encoding(fname.to_str().unwrap(), &content)
387
388    Ok(String::from_utf8_lossy(content).into_owned())
389}
390
391// Never fail
392#[cfg(all(feature = "chardetng", not(target_arch = "wasm32")))]
393pub fn handle_source_encoding(_fname: &str, content: &[u8]) -> Result<String, AssemblerError> {
394    let mut decoder = chardetng::EncodingDetector::new();
395    decoder.feed(content, true);
396    let encoding = decoder.guess(None, true);
397    let content = encoding.decode(content).0;
398
399    let content = content.into_owned();
400
401    Ok(content)
402}
403
404#[cfg(any(not(feature = "chardetng"), target_arch = "wasm32"))]
405pub fn handle_source_encoding(_fname: &str, _content: &[u8]) -> Result<String, AssemblerError> {
406    unimplemented!(
407        "i have deactivated this stuff to speed up everything. Let's consider each source is UTF8!"
408    )
409}
410
411// TODO add file saving functions and factorize code from other places