tx2dis/
main.rs

1#![deny(unsafe_code)]
2
3use base::prelude::*;
4use clap::ArgAction::Set;
5use clap::Parser;
6use std::error::Error;
7use std::ffi::{OsStr, OsString};
8use std::fmt::{self, Display, Formatter};
9use std::fs::OpenOptions;
10use std::io::{BufReader, Read};
11use tracing::{Level, span};
12use tracing_subscriber::prelude::*;
13
14// Thanks to Google for allowing this code to be open-sourced.  I
15// generally prefer to correspond about this project using my
16// personal email address rather than my work one, though.
17const AUTHOR: &str = "James Youngman <james@youngman.org>";
18const ABOUT: &str = "Disassembler for TX-2 punched-tape image files";
19
20/// Disassembler for punched-tape binaries for the historical TX-2 computer
21#[derive(Parser, Debug)]
22#[clap(author = AUTHOR, version, about=ABOUT, long_about = None)]
23struct Cli {
24    /// File from which the binary program (punched-tape image) is
25    /// read
26    #[clap(action=Set)]
27    input: OsString,
28}
29
30#[derive(Debug)]
31enum Fail {
32    ReadFailed(String),
33    Generic(String),
34    ShortFile,
35}
36
37impl Display for Fail {
38    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
39        match self {
40            Fail::Generic(message) | Fail::ReadFailed(message) => f.write_str(message),
41            Fail::ShortFile => f.write_str("file ended unexpectedly"),
42        }
43    }
44}
45
46impl Error for Fail {}
47
48fn update_sum(sum: Signed18Bit, word: Unsigned36Bit) -> Signed18Bit {
49    let (left, right) = split_halves(word);
50    sum.wrapping_add(right.reinterpret_as_signed())
51        .wrapping_add(left.reinterpret_as_signed())
52}
53
54fn read_splayed_words<R: Read>(
55    input: &mut R,
56    count: usize,
57    mut maybe_checksum: Option<&mut Signed18Bit>,
58) -> Result<Vec<Unsigned36Bit>, Fail> {
59    const SPLAY_SIZE: usize = 6;
60    let mut result: Vec<Unsigned36Bit> = Vec::with_capacity(count);
61    for _ in 0..count {
62        let mut w = Unsigned36Bit::ZERO;
63        let mut buf: [u8; SPLAY_SIZE] = [0; SPLAY_SIZE];
64        match input.read(&mut buf) {
65            Ok(0) => {
66                return Err(Fail::ShortFile);
67            }
68            Ok(nbytes) => {
69                if nbytes != SPLAY_SIZE {
70                    return Err(Fail::Generic(
71                        "input file length should be a multiple of 6 bytes".to_string(),
72                    ));
73                }
74                for byte in buf.into_iter() {
75                    let line: Unsigned6Bit = Unsigned6Bit::try_from(byte & 0o77).unwrap();
76                    w = cycle_and_splay(w, line);
77                }
78                if let Some(ref mut checksum) = maybe_checksum {
79                    **checksum = update_sum(**checksum, w);
80                }
81                result.push(w);
82            }
83            Err(e) => {
84                return Err(Fail::ReadFailed(e.to_string()));
85            }
86        }
87    }
88    Ok(result)
89}
90
91fn disassemble_block(mut pos: Unsigned18Bit, block: &[Unsigned36Bit]) {
92    let mut is_origin: bool = true;
93    for w in block.iter() {
94        disassemble_word(pos, *w, is_origin);
95        is_origin = false;
96        pos = pos.wrapping_add(Unsigned18Bit::ONE);
97    }
98}
99
100fn disassemble_word(loc: Unsigned18Bit, w: Unsigned36Bit, is_origin: bool) {
101    if is_origin {
102        print!("{loc:>06o}|");
103    } else {
104        print!("{:7}", "");
105    }
106    let raw = Instruction::from(w);
107    match SymbolicInstruction::try_from(&raw) {
108        Ok(symbolic) => {
109            print!("{:<20}", &symbolic.to_string());
110        }
111        Err(_) => {
112            print!("{:<20}", "");
113        }
114    }
115    let (left, right) = split_halves(w);
116    println!("|{left:>06o} {right:>06o}|{loc:>6o}");
117}
118
119fn disassemble_chunk<R: Read>(input: &mut R, checksum: &mut Signed18Bit) -> Result<bool, Fail> {
120    let first = read_splayed_words(input, 1, Some(checksum))?;
121    let (chunk_size, origin) = match first.as_slice() {
122        [only] => {
123            let (len_rep, end) = split_halves(*only);
124            let len = match Signed18Bit::ONE.checked_sub(len_rep.reinterpret_as_signed()) {
125                Some(n) => {
126                    if n.is_zero() {
127                        Some(Unsigned18Bit::ZERO)
128                    } else if n > Signed18Bit::ZERO {
129                        Some(n.reinterpret_as_unsigned())
130                    } else {
131                        None
132                    }
133                }
134                None => None,
135            }
136            .expect("overflow in block length");
137            println!("** {len}-word chunk ending at {end}");
138            let u_origin = end
139                .checked_sub(len)
140                .and_then(|n| n.checked_add(Unsigned18Bit::ONE))
141                .expect("overflow in origin calculation");
142            (u64::from(len) as usize, u_origin)
143        }
144        [] => return Err(Fail::ShortFile),
145        _ => unreachable!(),
146    };
147    let block: Vec<Unsigned36Bit> = read_splayed_words(input, chunk_size, Some(checksum))?;
148    let trailer: Vec<Unsigned36Bit> = read_splayed_words(input, 1, Some(checksum))?;
149    let (_adj, next) = match trailer.as_slice() {
150        [only] => split_halves(*only),
151        [] => return Err(Fail::ShortFile),
152        _ => unreachable!(),
153    };
154    disassemble_block(origin, &block);
155    {
156        let unsigned_checksum = checksum.reinterpret_as_unsigned();
157        if unsigned_checksum.is_zero() {
158            println!("** Checksum is valid: {unsigned_checksum:>06o}");
159        } else {
160            println!(
161                "** Checksum is not valid: {unsigned_checksum:>06o}, this tape cannot be loaded as-is",
162            );
163        }
164    }
165    if next == u18!(3) {
166        Ok(true)
167    } else if next == u18!(0o27) {
168        println!("** This is the final block");
169        Ok(false)
170    } else {
171        eprintln!("warning: block has unexpected next-address {next:o}");
172        Ok(false)
173    }
174}
175
176fn check_header<R: Read>(input: &mut R) -> Result<(), Fail> {
177    let expected_leader = assembler::reader_leader();
178    let header = read_splayed_words(input, expected_leader.len(), None)?;
179    for (pos, (want, got)) in expected_leader.iter().zip(header.iter()).enumerate() {
180        if want != got {
181            return Err(Fail::Generic(format!(
182                "File does not begin with the expected header; at position {pos} we expected {want:>012o} but got {got:>012o}"
183            )));
184        }
185    }
186    println!("** reader leader is valid:");
187    disassemble_block(Unsigned18Bit::from(3_u8), &header);
188    println!("** end of reader leader");
189    Ok(())
190}
191
192fn disassemble_file(input_file_name: &OsStr) -> Result<(), Fail> {
193    let input_file = OpenOptions::new()
194        .read(true)
195        .open(input_file_name)
196        .map_err(|e| Fail::Generic(format!("failed to open input file: {e}")))?;
197    let mut reader = BufReader::new(input_file);
198    check_header(&mut reader)?;
199    let mut checksum: Signed18Bit = Signed18Bit::ZERO;
200    loop {
201        match disassemble_chunk(&mut reader, &mut checksum) {
202            Ok(true) => (), // more data
203            Ok(false) => {
204                break;
205            }
206            Err(e) => {
207                return Err(e);
208            }
209        }
210    }
211    // The last block we read claimed it was the last.  Check that
212    // there is no more data.
213    let mut trailing_junk_count: usize = 0;
214    let mut buf = [0; 1];
215    loop {
216        match reader.read(&mut buf) {
217            Ok(0) => break,
218            Ok(n) => {
219                for junk in buf {
220                    eprintln!("trailing junk byte: {junk:o}");
221                }
222                trailing_junk_count = trailing_junk_count.saturating_add(n);
223            }
224            Err(e) => {
225                return Err(Fail::ReadFailed(e.to_string()));
226            }
227        }
228    }
229    if trailing_junk_count > 0 {
230        Err(Fail::Generic(
231            "input file contains more data after what was apparently the last block".to_string(),
232        ))
233    } else {
234        Ok(())
235    }
236}
237
238fn disassemble() -> Result<(), Fail> {
239    let cli = Cli::parse();
240    // See
241    // https://docs.rs/tracing-subscriber/0.2.19/tracing_subscriber/fmt/index.html#filtering-events-with-environment-variables
242    // for instructions on how to select which trace messages get
243    // printed.
244    let fmt_layer = tracing_subscriber::fmt::layer().with_target(true);
245    let filter_layer = match tracing_subscriber::EnvFilter::try_from_default_env()
246        .or_else(|_| tracing_subscriber::EnvFilter::try_new("info"))
247    {
248        Err(e) => {
249            return Err(Fail::Generic(format!(
250                "failed to initialise tracing filter (perhaps there is a problem with environment variables): {e}"
251            )));
252        }
253        Ok(layer) => layer,
254    };
255
256    tracing_subscriber::registry()
257        .with(filter_layer)
258        .with(fmt_layer)
259        .init();
260
261    let span = span!(Level::ERROR, "disassemble", input=?cli.input);
262    let _enter = span.enter();
263    disassemble_file(&cli.input)
264}
265
266fn main() {
267    match disassemble() {
268        Err(e) => {
269            eprintln!("error: {e}");
270            std::process::exit(1);
271        }
272        Ok(()) => {
273            std::process::exit(0);
274        }
275    }
276}