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
14const AUTHOR: &str = "James Youngman <james@youngman.org>";
18const ABOUT: &str = "Disassembler for TX-2 punched-tape image files";
19
20#[derive(Parser, Debug)]
22#[clap(author = AUTHOR, version, about=ABOUT, long_about = None)]
23struct Cli {
24 #[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) => (), Ok(false) => {
204 break;
205 }
206 Err(e) => {
207 return Err(e);
208 }
209 }
210 }
211 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 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}