summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Bridle <ben@derelict.engineering>2025-04-11 16:18:31 +1200
committerBen Bridle <ben@derelict.engineering>2025-04-11 16:18:31 +1200
commit6d016989b0ff5f600f3bdaced1ebf443d2f32da7 (patch)
treeb8732928eec01632e0cba965e6437718598f3251
parent2d29e79715c1208655316cce517713590894f5a6 (diff)
downloadtorque-asm-6d016989b0ff5f600f3bdaced1ebf443d2f32da7.zip
Add support for the CP/M CMD file module format
-rw-r--r--src/bin/tq.rs1
-rw-r--r--src/formats/cmd.rs54
-rw-r--r--src/formats/mod.rs13
3 files changed, 68 insertions, 0 deletions
diff --git a/src/bin/tq.rs b/src/bin/tq.rs
index b27f702..ffbc330 100644
--- a/src/bin/tq.rs
+++ b/src/bin/tq.rs
@@ -172,6 +172,7 @@ Created by Ben Bridle.
if !dry_run {
let result = match format {
+ Format::Cmd => format_cmd(&segments),
Format::Debug => format_debug(&segments),
Format::Inhx => format_inhx(&segments),
Format::Inhx32 => format_inhx32(&segments),
diff --git a/src/formats/cmd.rs b/src/formats/cmd.rs
new file mode 100644
index 0000000..2ff2097
--- /dev/null
+++ b/src/formats/cmd.rs
@@ -0,0 +1,54 @@
+// CP/M CMD format, also the TRS-80 Load Module Format (LMF)
+// https://en.wikipedia.org/wiki/CMD_file_(CP/M)
+// https://www.tim-mann.org/trs80/doc/ldosq1-4.pdf (page 43)
+use crate::*;
+
+
+pub fn format_cmd(segments: &[Segment]) -> Result<Vec<u8>, FormatError> {
+ let mut records = Vec::new();
+ let mut address;
+ for segment in segments {
+ address = segment.address;
+ for chunk in segment.words.chunks(16) {
+ records.push(data_record(chunk, address)?);
+ address += 16;
+ }
+ }
+ let start_address = segments.first().map(|s| s.address).unwrap_or(0);
+ records.push(terminating_record(start_address)?);
+
+ let mut output = String::new();
+ for record in records {
+ output.push_str(&record.to_string_plain());
+ }
+ return Ok(output.into_bytes());
+}
+
+fn data_record(words: &[Tracked<Word>], address: usize) -> Result<InhxRecord, FormatError> {
+ let Ok(address) = u16::try_from(address) else {
+ return Err(FormatError::AddressTooLarge(u16::MAX as usize, address));
+ };
+ let mut record = InhxRecord::new();
+ record.byte(0x01);
+ let data_bytes = words.len() as u8;
+ record.byte(data_bytes.wrapping_add(2));
+ record.le_double(address);
+ for word in words {
+ if word.value.width > 8 {
+ return Err(FormatError::WordTooWide(8, word.width, word.source.clone()));
+ }
+ record.byte(word.value.value as u8);
+ }
+ return Ok(record);
+}
+
+fn terminating_record(address: usize) -> Result<InhxRecord, FormatError> {
+ let Ok(address) = u16::try_from(address) else {
+ return Err(FormatError::AddressTooLarge(u16::MAX as usize, address));
+ };
+ let mut record = InhxRecord::new();
+ record.byte(0x02);
+ record.byte(0x02);
+ record.le_double(address);
+ return Ok(record);
+}
diff --git a/src/formats/mod.rs b/src/formats/mod.rs
index a77bd72..105207c 100644
--- a/src/formats/mod.rs
+++ b/src/formats/mod.rs
@@ -1,8 +1,10 @@
+mod cmd;
mod inhx;
mod inhx32;
mod raw;
mod debug;
+pub use cmd::*;
pub use inhx::*;
pub use inhx32::*;
pub use raw::*;
@@ -15,6 +17,7 @@ use log::*;
#[derive(Clone, Copy, PartialEq)]
pub enum Format {
+ Cmd,
Debug,
Inhx,
Inhx32,
@@ -25,6 +28,7 @@ pub enum Format {
impl Format {
pub fn from_str(string: &str) -> Self {
match string {
+ "cmd" => Self::Cmd,
"debug" => Self::Debug,
"inhx" => Self::Inhx,
"inhx32" => Self::Inhx32,
@@ -38,6 +42,7 @@ impl Format {
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
let string = match self {
+ Self::Cmd => "cmd",
Self::Debug => "debug",
Self::Inhx => "inhx",
Self::Inhx32 => "inhx32",
@@ -112,6 +117,14 @@ impl InhxRecord {
}
format!(":{output}{checksum:0>2X}\n")
}
+
+ pub fn to_string_plain(self) -> String {
+ let mut output = String::new();
+ for byte in &self.bytes {
+ output.push_str(&format!("{byte:0>2X}"));
+ }
+ format!("{output}\n")
+ }
}