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 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 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}