matrix65/
io.rs

1// copyright 2022 mikael lund aka wombat
2//
3// licensed under the apache license, version 2.0 (the "license");
4// you may not use this file except in compliance with the license.
5// you may obtain a copy of the license at
6//
7//     http://www.apache.org/licenses/license-2.0
8//
9// unless required by applicable law or agreed to in writing, software
10// distributed under the license is distributed on an "as is" basis,
11// without warranties or conditions of any kind, either express or implied.
12// see the license for the specific language governing permissions and
13// limitations under the license.
14
15//! Routines for file; url; and terminal I/O
16
17use anyhow::Result;
18use cbm::disk;
19use cbm::disk::file::FileOps;
20use disasm6502;
21use log::debug;
22use std::fs::File;
23use std::io::{self, Read, Write};
24use tempfile::Builder;
25
26use crate::LoadAddress;
27
28/// Fill byte vector from url with compatible error
29fn load_bytes_url(url: &str) -> Result<Vec<u8>> {
30    Ok(reqwest::blocking::get(url)?.bytes()?.to_vec())
31}
32
33/// Load file or url into byte vector
34pub fn load_bytes(filename: &str) -> Result<Vec<u8>> {
35    let mut bytes = Vec::new();
36    if filename.starts_with("http") {
37        bytes = load_bytes_url(filename)?;
38    } else {
39        File::open(&filename)?.read_to_end(&mut bytes)?;
40    }
41    assert!(bytes.len() < 0xffff);
42    Ok(bytes)
43}
44
45/// Load PRG from prg and CBM disk files
46///
47/// If an archive (.d64|.d81) is detected, the user is presented with a selection
48/// of found PRG files. Returns intended load address and raw bytes.
49pub fn load_prg(file: &str) -> Result<(LoadAddress, Vec<u8>)> {
50    match std::path::Path::new(&file).extension() {
51        None => load_with_load_address(file),
52        Some(os_str) => match os_str.to_ascii_lowercase().to_str() {
53            Some("prg") => load_with_load_address(file),
54            Some("d81") | Some("d64") => cbm_select_and_load(file),
55            _ => Err(anyhow::Error::msg("invalid file extension")),
56        },
57    }
58}
59
60/// Purge and return load address from vector of bytes
61///
62/// The two first bytes form the 16-bit load address, little endian.
63/// Returns found address and removes the first two bytes from the byte vector.
64///
65/// Example:
66/// ~~~
67/// let mut bytes : Vec<u8> = vec![0x01, 0x08, 0xff];
68/// let load_address = matrix65::io::purge_load_address(&mut bytes).unwrap();
69/// assert_eq!(load_address.value(), 0x0801);
70/// assert_eq!(bytes.len(), 1);
71/// assert_eq!(bytes[0], 0xff);
72/// ~~~
73pub fn purge_load_address(bytes: &mut Vec<u8>) -> Result<LoadAddress> {
74    let address = LoadAddress::from_bytes(bytes)?;
75    *bytes = bytes[2..].to_vec();
76    Ok(address)
77}
78
79/// Open a CBM disk image from file or url
80pub fn cbm_open(diskimage: &str) -> Result<Box<dyn cbm::disk::Disk>> {
81    debug!("Opening CBM disk {}", diskimage);
82    if diskimage.starts_with("http") {
83        let bytes = load_bytes_url(diskimage)?;
84        let tmp_dir = Builder::new().tempdir()?;
85        let path = tmp_dir.path().join("temp-image");
86        let filename = path.to_str().unwrap_or("");
87        save_binary(filename, &bytes)?;
88        Ok(disk::open(filename, false)?)
89    } else {
90        Ok(disk::open(diskimage, false)?)
91    }
92}
93
94/// Load n'th file from CBM disk image and return load address and bytes
95pub fn cbm_load_file(disk: &dyn cbm::disk::Disk, index: usize) -> Result<(LoadAddress, Vec<u8>)> {
96    let dir = disk.directory()?;
97    let entry = dir
98        .get(index)
99        .ok_or_else(|| anyhow::Error::msg("invalid selection"))?;
100    let mut bytes = Vec::<u8>::new();
101    disk.open_file(&entry.filename)?
102        .reader()?
103        .read_to_end(&mut bytes)?;
104    let load_address = purge_load_address(&mut bytes)?;
105    Ok((load_address, bytes))
106}
107
108/// User select PRG file from CBM image file or url
109///
110/// Looks for PRG files on the CBM disk image and
111/// presents a numbered list from which the user
112/// can select. Loads the file and returns the load
113/// address together with raw bytes.
114fn cbm_select_and_load(diskimage: &str) -> Result<(LoadAddress, Vec<u8>)> {
115    let disk = cbm_open(diskimage)?;
116    let dir = disk.directory()?;
117    let prg_files = &mut dir
118        .iter()
119        .filter(|entry| entry.file_attributes.file_type == cbm::disk::directory::FileType::PRG);
120    for (counter, file) in prg_files.clone().enumerate() {
121        println!("[{}] {}.prg", counter, file.filename.to_string());
122    }
123    print!("Select: ");
124    io::stdout().flush()?;
125    let mut selection = String::new();
126    io::stdin().read_line(&mut selection)?;
127    let index = selection.trim_end().parse::<usize>()?;
128
129    let entry = prg_files
130        .nth(index)
131        .ok_or_else(|| anyhow::Error::msg("invalid selection"))?;
132    let mut bytes = Vec::<u8>::new();
133    disk.open_file(&entry.filename)?
134        .reader()?
135        .read_to_end(&mut bytes)?;
136    let load_address = purge_load_address(&mut bytes)?;
137    Ok((load_address, bytes))
138}
139
140/// Load a prg file or url into a byte vector and detect load address
141pub fn load_with_load_address(filename: &str) -> Result<(LoadAddress, Vec<u8>)> {
142    let mut bytes = load_bytes(filename)?;
143    let load_address = purge_load_address(&mut bytes)?;
144    debug!(
145        "Read {} bytes from {}; detected load address = 0x{:x}",
146        bytes.len() + 2,
147        &filename,
148        load_address.value()
149    );
150    Ok((load_address, bytes.to_vec()))
151}
152
153/// Save bytes to binary file
154pub fn save_binary(filename: &str, bytes: &[u8]) -> Result<(), std::io::Error> {
155    debug!("Saving {} bytes to {}", bytes.len(), filename);
156    File::create(filename)?.write_all(bytes)
157}
158
159/// Print bytes to screen
160pub fn hexdump(bytes: &[u8], bytes_per_line: usize) {
161    let to_hex = |i: u8| format!("0x{:02x}", i);
162    bytes.chunks(bytes_per_line).for_each(|line| {
163        for byte in line {
164            print!("{} ", to_hex(*byte));
165        }
166        println!();
167    });
168}
169/// Print disassembled bytes
170pub fn disassemble(bytes: &[u8], start_address: u32) {
171    let instructions = disasm6502::from_addr_array(bytes, start_address as u16).unwrap();
172    for i in instructions {
173        println!("{}", i);
174    }
175}