Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions crates/volamos-core/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2767,11 +2767,9 @@ impl<C: Cpu + 'static> Runtime<C> {
// startup may read this instead of (or in addition to) A0/D0.
mem.write_u32(task + exectask::PR_ARGUMENTS_OFFSET, args_addr);

// ReadArgs's default source (rdargs == NULL): the same
// command-line buffer just built above, per crate::dosargs's
// module docs. Registers hold this too, but a guest program is
// free to clobber A0/D0 before ever calling ReadArgs.
dos.cmdline = Some((args_addr, line_len));
// The same text in the Input() buffer, where ReadArgs reads it,
// as RunCommand puts it there on AmigaOS: see DosState::input_buf.
dos.input_buf = line.clone();

Self {
cpu,
Expand Down Expand Up @@ -4675,6 +4673,19 @@ mod tests {
);
}

#[test]
fn the_command_line_is_also_in_the_input_buffer_for_readargs() {
// vbcc's startup code writes a NUL over the command line's '\n'
// as it splits the A0 buffer into argv; ReadArgs, reading the A0
// buffer, then saw "LIST\0" where the user typed LIST. It reads
// the Input() buffer now, as on AmigaOS.
let mut rt = runtime_with_program_and_args(&[RTS], vec!["LIST".to_string()]);
let a0 = rt.cpu.address_register(AddressRegister(0));
let d0 = rt.cpu.data_register(DataRegister(0));
rt.mem.write_u8(a0 + d0 - 1, 0);
assert_eq!(rt.dos.input_buf, b"LIST \n");
}

#[test]
fn empty_args_still_produce_a_bare_newline_command_line() {
let rt = runtime_with_program_and_args(&[RTS], Vec::new());
Expand Down
48 changes: 20 additions & 28 deletions crates/volamos-core/src/dosargs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
//!
//! # Scope and deviations from real `ReadArgs`
//!
//! - **Default source only.** `rdargs == NULL` reads from
//! [`DosState::cmdline`] -- the same `A0`/`D0` command-line buffer
//! [`crate::dispatch::Runtime::new`] builds, exactly mirroring how real
//! AmigaOS delivers the CLI command tail through the process's buffered
//! `Input()`. [`DosState::cmdline_pos`] persists across calls, so
//! repeated `ReadArgs(NULL)` calls in one process walk forward through
//! the same buffer rather than each re-parsing it from the start --
//! real `ReadArgs(NULL)` shares this property because it reads off a
//! stateful buffered stream. A caller-supplied `rdargs` with its own
//! - **Default source only.** `rdargs == NULL` reads the command line
//! `RunCommand` stuffed into the buffer of `Input()`,
//! [`DosState::input_buf`], as real `ReadArgs` "normally gets the
//! arguments by reading buffered IO from Input()" (autodoc). Reading
//! advances [`DosState::input_pos`], which buffered reads share, so
//! repeated `ReadArgs(NULL)` calls walk forward through the stream
//! rather than each re-parsing it from the start. Once that buffer is
//! used up, `ReadArgs` sees an empty line rather than waiting on host
//! stdin, as AROS does for an interactive `Input()` with an empty
//! buffer. A caller-supplied `rdargs` with its own
//! `RDA_Source` (a custom string to parse instead of the command line)
//! is not implemented: `rdargs` is accepted only as an *identity* for
//! `FreeArgs` bookkeeping (see below), and parsing still always reads
Expand Down Expand Up @@ -567,15 +568,10 @@ fn read_args(
) -> Result<u32, i32> {
let template = parse_template(template_bytes)?;

let (cmdline_addr, cmdline_len) = dos.cmdline.unwrap_or((0, 0));
let mut buf = Vec::with_capacity(cmdline_len as usize);
for offset in 0..cmdline_len {
buf.push(mem.read_u8(cmdline_addr.wrapping_add(offset)));
}

let mut reader = Reader::new(&buf, dos.cmdline_pos as usize);
let buf = dos.input_buf.clone();
let mut reader = Reader::new(&buf, dos.input_pos);
let result = match_args(&mut reader, &template);
dos.cmdline_pos = reader.pos as u32;
dos.input_pos = reader.pos;

let slots = result?;
let allocations = materialize(heap, mem, &template, slots, array_addr)?;
Expand Down Expand Up @@ -685,7 +681,7 @@ fn find_arg_handler<C: Cpu>(ctx: &mut HandlerContext<'_, C>) -> Result<(), Dispa
/// Registers `ReadArgs`/`FreeArgs` onto [`DOS_LIBRARY_BASE`], looked up
/// by name through [`DOS_LVOS`]. Called from [`crate::dispatch::
/// Runtime::new`] alongside the other `dos.library` registrations; these
/// handlers need [`DosState::cmdline`] (always set by `Runtime::new`),
/// handlers need [`DosState::input_buf`] (always set by `Runtime::new`),
/// not a `Vfs`, so they work regardless of whether one is installed.
pub fn register_dosargs_handlers<C: Cpu + 'static>(
table: &mut LibraryTable<C>,
Expand Down Expand Up @@ -728,20 +724,16 @@ mod tests {
use super::*;
use crate::memory::FlatMemory;

/// Builds a fresh `(heap, mem, dos)` triple with `dos.cmdline`
/// pointing at `cmdline` (space-joined-and-newline-terminated, same
/// framing `Runtime::new` gives the real `A0`/`D0` buffer).
/// Builds a fresh `(heap, mem, dos)` triple with `cmdline` in
/// `dos.input_buf` (space-joined and newline-terminated, the framing
/// `Runtime::new` gives it).
fn setup(cmdline: &[&str]) -> (GuestHeap, FlatMemory, DosState) {
let mut mem = FlatMemory::new(0x4000);
let mut heap = GuestHeap::new(0x100, 0x3000);
let mem = FlatMemory::new(0x4000);
let heap = GuestHeap::new(0x100, 0x3000);
let mut line = cmdline.join(" ").into_bytes();
line.push(b'\n');
let addr = heap.alloc(&mut mem, line.len() as u32).unwrap();
for (i, &b) in line.iter().enumerate() {
mem.write_u8(addr + i as u32, b);
}
let mut dos = DosState::new(None);
dos.cmdline = Some((addr, line.len() as u32));
dos.input_buf = line;
(heap, mem, dos)
}

Expand Down
28 changes: 14 additions & 14 deletions crates/volamos-core/src/dosbuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
//! `FGetC` consults it before ever touching the host file, so a
//! `FGetC`/`UnGetC`/`FGetC` sequence round-trips correctly.
//!
//! The other is the command line `RunCommand` stuffs into the buffer of
//! `Input()` (`crate::dosfile::DosState::input_buf`): buffered reads of
//! the current `Input()` return it before any host stdin, as they do on
//! AmigaOS, where `ReadArgs` reads its arguments that way.
//!
//! `SelectInput`/`SelectOutput` (redirecting a *process's* default
//! `Input()`/`Output()` handle) and interactive-file-specific behavior
//! (`IsInteractive`, the BCPL command-line-parameters-in-the-input-
//! buffer quirk `files.md` documents for `Input()`) are out of scope --
//! this runtime's `Input()` is backed by real host stdin already (see
//! `crate::dosfile`'s module docs), not a command-line buffer, so that
//! particular real-AmigaOS quirk doesn't apply here in the first place.
//! `Input()`/`Output()` handle) and other interactive-file-specific
//! behavior (`IsInteractive`) are out of scope.

use crate::cpu::{Cpu, DataRegister};
use crate::dispatch::{DOS_LIBRARY_BASE, DispatchError, HandlerContext, LibraryTable};
Expand All @@ -54,9 +55,9 @@ fn fgetc(ctx: &mut HandlerContext<'_, impl Cpu>, addr: u32) -> i32 {
let value = if let Some(pushed) = ctx.dos.ungetc_buf.remove(&addr) {
pushed
} else {
match ctx.dos.read(addr, 1) {
Ok(bytes) if !bytes.is_empty() => i32::from(bytes[0]),
Ok(_) => {
match ctx.dos.read_buffered_byte(addr) {
Ok(Some(byte)) => i32::from(byte),
Ok(None) => {
ctx.dos.set_io_err(0); // clean EOF, not an error
ENDSTREAMCH
}
Expand Down Expand Up @@ -155,13 +156,12 @@ fn fread_handler<C: Cpu>(ctx: &mut HandlerContext<'_, C>) -> Result<(), Dispatch
let total_wanted = (block_len as u64) * (block_count as u64);
let mut got = 0u64;
while got < total_wanted {
match ctx.dos.read(addr, 1) {
Ok(bytes) if !bytes.is_empty() => {
ctx.mem
.write_u8(buf_addr.wrapping_add(got as u32), bytes[0]);
match ctx.dos.read_buffered_byte(addr) {
Ok(Some(byte)) => {
ctx.mem.write_u8(buf_addr.wrapping_add(got as u32), byte);
got += 1;
}
Ok(_) => break, // EOF
Ok(None) => break, // EOF
Err(code) => {
ctx.dos.set_io_err(code);
break;
Expand Down
67 changes: 51 additions & 16 deletions crates/volamos-core/src/dosfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,20 +442,25 @@ pub struct DosState {
// the sibling module" convention as the T11/Phase-3-stage-7 fields
// above), but the fields themselves are declared here per that same
// convention.
/// Guest address of the process's command-line buffer (the `A0`/`D0`
/// buffer [`crate::dispatch::Runtime::new`] builds from
/// [`crate::dispatch::StartConfig::args`]) and its length in bytes
/// (including the trailing `'\n'`). This is `ReadArgs`'s default
/// input source (`rdargs == NULL`), mirroring how real AmigaOS
/// delivers the CLI command tail through the process's buffered
/// `Input()` -- see `crate::dosargs`'s module docs.
pub(crate) cmdline: Option<(u32, u32)>,
/// Byte offset into `cmdline` that the next default-source `ReadArgs`
/// call resumes from -- real `ReadArgs(NULL)` reads from a shared,
/// stateful input stream, so repeated calls in one process walk
/// forward through the same buffer rather than each re-parsing it
/// from the start.
pub(crate) cmdline_pos: u32,
/// The command line, as `RunCommand` stuffs it into the buffer of
/// the process's `Input()`, including the trailing `'\n'`: the text
/// of the `A0`/`D0` buffer [`crate::dispatch::Runtime::new`] builds
/// from [`crate::dispatch::StartConfig::args`], or of the arguments
/// `RunCommand` gets. The `RunCommand` autodoc: it "takes care of
/// setting up the current input filehandle in such a way that
/// ReadArgs() can be used in the program, and restores the state of
/// the buffering before returning"; `ReadArgs` "normally gets the
/// arguments by reading buffered IO from Input()". AROS does the same
/// (`rom/dos/runcommand.c`, `vbuf_inject()`), after the Guru Book.
///
/// So `ReadArgs(NULL)` and buffered reads of the current `Input()`
/// (`FGetC`, `FGets`, `FRead`) take these bytes before the host
/// stream, and the guest's `A0` buffer is not involved: C startup
/// code splits that into argv in place (vbcc's writes a NUL over the
/// `'\n'`), which real `ReadArgs` never sees.
pub(crate) input_buf: Vec<u8>,
/// How much of `input_buf` has been read.
pub(crate) input_pos: usize,
/// Live `ReadArgs` results, keyed by the `struct RDArgs*` anchor
/// address returned in `D0` (see `crate::dosargs::RDARGS_STRUCT_SIZE`).
/// `FreeArgs` looks up and frees every heap block an entry lists, plus
Expand Down Expand Up @@ -545,8 +550,8 @@ impl DosState {
seglists: HashMap::new(),
overlay_roots: HashMap::new(),
system_runner: None,
cmdline: None,
cmdline_pos: 0,
input_buf: Vec::new(),
input_pos: 0,
rdargs: HashMap::new(),
local_vars: HashMap::new(),
anchor_states: HashMap::new(),
Expand Down Expand Up @@ -962,6 +967,18 @@ impl DosState {
}
}

/// One byte of buffered input from the handle at `addr`, `None` at
/// EOF: the rest of [`Self::input_buf`] first when `addr` is the
/// current `Input()`, then the handle itself. Plain `Read` bypasses
/// the buffer, as it does on AmigaOS, so it uses [`Self::read`].
pub fn read_buffered_byte(&mut self, addr: u32) -> Result<Option<u8>, i32> {
if self.current_input == Some(addr) && self.input_pos < self.input_buf.len() {
self.input_pos += 1;
return Ok(Some(self.input_buf[self.input_pos - 1]));
}
Ok(self.read(addr, 1)?.first().copied())
}

/// `Read`: reads up to `len` bytes from the handle at `addr`,
/// returning the bytes actually read (may be shorter than `len`,
/// including empty at EOF -- matching real `Read`'s "actual count"
Expand Down Expand Up @@ -2280,6 +2297,24 @@ mod tests {
assert_eq!(err, ERROR_INVALID_LOCK);
}

#[test]
fn buffered_reads_of_input_return_the_command_line_first() {
// RunCommand stuffs the arguments into the buffer of Input(), so
// FGetC(Input()) sees them before anything else; another handle
// does not.
let mut heap = GuestHeap::new(0x1000, 0x4000);
let mut mem = FlatMemory::new(0x4000);
let mut dos = DosState::new(None);
dos.input_buf = b"A\n".to_vec();
let input = dos.input_addr(&mut heap, &mut mem).unwrap();
let other = dos.alloc_file_handle(&mut heap, &mut mem, false).unwrap();

assert_eq!(dos.read_buffered_byte(other), Err(ERROR_INVALID_LOCK));
assert_eq!(dos.read_buffered_byte(input), Ok(Some(b'A')));
assert_eq!(dos.read_buffered_byte(input), Ok(Some(b'\n')));
assert_eq!(dos.input_pos, 2);
}

#[test]
fn open_into_handle_resolves_relative_to_a_parent_lock_and_creates_the_file() {
let tmp = TempDir::new("open-into-handle");
Expand Down
Loading
Loading