From 6a0d320a378158efa139c5c9be0d91a0a52418db Mon Sep 17 00:00:00 2001 From: Bernie Innocenti Date: Fri, 23 Oct 2026 15:28:18 +0900 Subject: [PATCH 1/2] dos.library: GetDeviceProc returns the filesystem packet port in dvp_Port ixemul.library sends every path operation to dvp_Port and fails with ERROR_OBJECT_NOT_FOUND when it is NULL; locks and handles already name the port #191 answers packets on. Co-authored-by: Claude Opus 5.5 --- crates/volamos-core/src/dosdevproc.rs | 59 ++++++++++++++++----------- crates/volamos-core/src/dosfile.rs | 3 +- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/crates/volamos-core/src/dosdevproc.rs b/crates/volamos-core/src/dosdevproc.rs index ba274bb..8b1663a 100644 --- a/crates/volamos-core/src/dosdevproc.rs +++ b/crates/volamos-core/src/dosdevproc.rs @@ -5,26 +5,26 @@ //! binary: it calls `GetDeviceProc` (after `Lock`+`CurrentDir` on the //! target's containing directory) before deleting anything. //! -//! # Scope: no real handler processes or packets +//! # The handler port //! //! Real `GetDeviceProc` returns a `struct DevProc` whose `dvp_Port` is //! the real `MsgPort` of the handler/file-system process responsible -//! for the path -- callers that go on to build and send a `DosPacket` -//! to it expect a live process answering on the other end. This -//! runtime has no such processes (see `crate::execlist`'s module docs: -//! its message-port primitives are single-threaded scaffolding, not a -//! real multi-task packet round-trip), so `dvp_Port` is always `0` -//! (`NULL`) here. What this runtime *can* do faithfully is the locking -//! half: `dvp_Lock`, a [`crate::doslock::SHARED_LOCK`] on the target's -//! containing directory -- confirmed as `GetDeviceProc`'s actual role -//! for e.g. `CreateDir()` internally, per the RKRM's own packet -//! documentation ("The lock in `dp_Arg1` is the directory to which the -//! path in `dp_Arg2` is relative. The `CreateDir()` function obtains it -//! from `GetDeviceProc()`"). A corpus binary that only uses the -//! returned lock (as opposed to sending it raw packets over -//! `dvp_Port`) works correctly against this; one that needs the packet -//! round-trip will hit the next unhandled call at `dvp_Port` usage -//! instead of crashing silently on a `NULL`-dereference here. +//! for the path, and callers go on to send `DosPacket`s to it. This +//! runtime has no handler processes, but it has one filesystem packet +//! port ([`crate::dosfile::DosState::fs_port_addr`]) whose `PutMsg` is +//! answered host-side by [`crate::dospkt::handle_fs_packet`] (issue +//! #186); every lock's `fl_Task` and every handle's `fh_Type` already +//! name it, and `dvp_Port` does too. ixemul.library resolves every path +//! that way (`__plock()`: `GetDeviceProc`, then `ACTION_LOCATE_OBJECT` +//! and the action itself to `dvp_Port`), and gives up with +//! `ERROR_OBJECT_NOT_FOUND` when `dvp_Port` is `NULL`. +//! +//! `dvp_Lock` is a [`crate::doslock::SHARED_LOCK`] on the target's +//! containing directory -- `GetDeviceProc`'s actual role for e.g. +//! `CreateDir()` internally, per the RKRM's own packet documentation +//! ("The lock in `dp_Arg1` is the directory to which the path in +//! `dp_Arg2` is relative. The `CreateDir()` function obtains it from +//! `GetDeviceProc()`"). //! //! Multi-assign iteration (`GetDeviceProc` called again with a //! previous, non-`NULL` result to advance to the next directory of a @@ -97,9 +97,18 @@ fn get_device_proc( }; let lock_bptr = dos.lock(heap, mem, &dir_part, SHARED_LOCK)?; + // The packet port every lock's fl_Task and handle's fh_Type name: + // see the module docs. + let port = match dos.fs_port_addr(heap, mem) { + Ok(port) => port, + Err(code) => { + dos.unlock(heap, addr_from_bptr(lock_bptr)); + return Err(code); + } + }; match heap.alloc(&mut *mem, DEVPROC_SIZE) { Ok(addr) => { - mem.write_u32(addr + DVP_PORT_OFFSET, 0); + mem.write_u32(addr + DVP_PORT_OFFSET, port); mem.write_u32(addr + DVP_LOCK_OFFSET, lock_bptr); mem.write_u32(addr + DVP_FLAGS_OFFSET, 0); mem.write_u32(addr + DVP_DEVNODE_OFFSET, 0); @@ -230,10 +239,10 @@ mod tests { let addr = get_device_proc(&mut heap, &mut mem, &mut dos, "SYS:work/f.txt", 0).unwrap(); assert_ne!(addr, 0); - assert_eq!( - mem.read_u32(addr + DVP_PORT_OFFSET), - 0, - "no real handler process" + let port = mem.read_u32(addr + DVP_PORT_OFFSET); + assert!( + dos.is_fs_handler_port(port), + "dvp_Port should be the filesystem packet port" ); let lock_bptr = mem.read_u32(addr + DVP_LOCK_OFFSET); assert_ne!(lock_bptr, 0); @@ -379,7 +388,11 @@ mod tests { assert_ne!(code, 0, "should return a non-NULL DevProc*"); let addr = code as u32; - assert_eq!(rt.memory().read_u32(addr + DVP_PORT_OFFSET), 0); + assert_ne!( + rt.memory().read_u32(addr + DVP_PORT_OFFSET), + 0, + "dvp_Port should be the filesystem packet port" + ); let lock_bptr = rt.memory().read_u32(addr + DVP_LOCK_OFFSET); assert_ne!(lock_bptr, 0, "dvp_Lock should be a real lock"); } diff --git a/crates/volamos-core/src/dosfile.rs b/crates/volamos-core/src/dosfile.rs index 02a51a1..0fdf3dd 100644 --- a/crates/volamos-core/src/dosfile.rs +++ b/crates/volamos-core/src/dosfile.rs @@ -2060,8 +2060,7 @@ pub(crate) const DEFAULT_FILE_SYS_TASK: u32 = 1; /// why raw packets are out of scope), never to actually dereference it, /// so a real backing `MsgPort` struct isn't needed. Real /// `GetFileSysTask` never returns `NULL` on a booted system, so `0` -/// would be the wrong choice here (unlike [`crate::dosdevproc`]'s -/// `dvp_Port`, which real callers do check for `NULL`)). Doesn't touch +/// would be the wrong choice here). Doesn't touch /// `IoErr()`, matching the real function. fn get_file_sys_task_handler(ctx: &mut HandlerContext<'_, C>) -> Result<(), DispatchError> { ctx.cpu From 3f1fa4e42ed6e679c9038fe88bfd9d6437ec4007 Mon Sep 17 00:00:00 2001 From: Bernie Innocenti Date: Fri, 23 Oct 2026 16:11:45 +0900 Subject: [PATCH 2/2] dos.library: the path packets ixemul.library sends, and Info() CREATE_DIR, DELETE_OBJECT, RENAME_OBJECT, SET_PROTECT, SET_DATE, SET_OWNER, MAKE_LINK, SET_FILE_SIZE, DISK_INFO and WAIT_CHAR, over the code of the matching dos.library calls; an unknown action now replies DOSFALSE, which no boolean caller mistakes for success. Fixes #196. Co-authored-by: Claude Opus 5.5 --- crates/volamos-core/src/dospkt.rs | 503 +++++++++++++++++++++- crates/volamos-core/src/dosprotect.rs | 53 +-- crates/volamos-core/src/dossetfiledate.rs | 58 +-- 3 files changed, 560 insertions(+), 54 deletions(-) diff --git a/crates/volamos-core/src/dospkt.rs b/crates/volamos-core/src/dospkt.rs index 8cd240e..d002f27 100644 --- a/crates/volamos-core/src/dospkt.rs +++ b/crates/volamos-core/src/dospkt.rs @@ -95,6 +95,23 @@ const ACTION_LOCATE_OBJECT: u32 = 8; const ACTION_EXAMINE_FH: u32 = 1034; const ACTION_READ_LINK: u32 = 1024; +// Issue #196: the rest of what ixemul.library sends for paths (mkdir, +// unlink/rmdir, rename, chmod, utime, chown, link/symlink, statfs, and +// select() on a console), each over the same DosState code the +// matching dos.library call uses. +const ACTION_CREATE_DIR: u32 = 22; +const ACTION_DELETE_OBJECT: u32 = 16; +const ACTION_RENAME_OBJECT: u32 = 17; +const ACTION_SET_PROTECT: u32 = 21; +const ACTION_SET_DATE: u32 = 34; +const ACTION_SET_OWNER: u32 = 1036; +const ACTION_MAKE_LINK: u32 = 1021; +const ACTION_SET_FILE_SIZE: u32 = 1022; +const ACTION_WAIT_CHAR: u32 = 20; + +/// `ACTION_MAKE_LINK`'s `dp_Arg4` for a soft link (`LINK_SOFT`, dos/dos.h). +const LINK_SOFT: u32 = 1; + // --- struct DosPacket field offsets (dos/dosextens.h) -- all LONGs. --- /// `dp_Port`: the `MsgPort` the handler sends the packet back to once @@ -138,6 +155,36 @@ const DOSFALSE: u32 = 0; /// things. const RESULT_ERROR: u32 = 0xFFFF_FFFF; +/// The Amiga path a path packet names: the BSTR at `name_bptr`, +/// relative to the lock at `lock_bptr` (`0` for the current volume's +/// root), as for `ACTION_LOCATE_OBJECT`. +fn path_arg( + dos: &crate::dosfile::DosState, + mem: &dyn AddressSpace, + lock_bptr: u32, + name_bptr: u32, +) -> Result { + let name = String::from_utf8_lossy(&read_bstr(mem, addr_from_bptr(name_bptr))).into_owned(); + dos.combine_lock_path(addr_from_bptr(lock_bptr), &name) +} + +/// `Res1`/`Res2` for a boolean action. +fn bool_result(result: Result<(), i32>) -> (u32, u32) { + match result { + Ok(()) => (DOSTRUE, 0), + Err(code) => (DOSFALSE, code as u32), + } +} + +/// Whether `path` names an existing object, as `ERROR_OBJECT_NOT_FOUND` +/// (or the VFS's own error) when it does not. +fn object_exists(dos: &crate::dosfile::DosState, path: &str) -> Result<(), i32> { + let vfs = dos.vfs.as_ref().ok_or(ERROR_OBJECT_NOT_FOUND)?; + vfs.resolve(path, crate::vfs::ResolveMode::MustExist) + .map(|_| ()) + .map_err(|e| crate::dosfile::map_vfs_error(&e)) +} + fn fill_info_data(mem: &mut dyn AddressSpace, addr: u32) { mem.write_u32(addr + ID_NUMSOFTERRORS_OFFSET, 0); mem.write_u32(addr + ID_UNITNUMBER_OFFSET, 0); @@ -385,7 +432,90 @@ pub fn handle_fs_packet( Err(_) => (RESULT_ERROR, ERROR_OBJECT_NOT_FOUND as u32), } } - _ => (RESULT_ERROR, ERROR_ACTION_NOT_KNOWN as u32), + // ACTION_CREATE_DIR (22): Arg1 = parent lock, Arg2 = BSTR name. + // Res1 = an exclusive lock on the new directory, or 0. + ACTION_CREATE_DIR => match path_arg(ctx.dos, ctx.mem, arg1, arg2) + .and_then(|path| ctx.dos.create_dir(ctx.heap, ctx.mem, &path)) + { + Ok(bptr) => (bptr, 0), + Err(code) => (DOSFALSE, code as u32), + }, + // ACTION_DELETE_OBJECT (16): Arg1 = parent lock, Arg2 = BSTR + // name. Res1 = boolean. + ACTION_DELETE_OBJECT => bool_result( + path_arg(ctx.dos, ctx.mem, arg1, arg2).and_then(|path| ctx.dos.delete_file(&path)), + ), + // ACTION_RENAME_OBJECT (17): Arg1/Arg2 = lock and BSTR name of + // the object, Arg3/Arg4 = lock and BSTR name of the new name. + // Res1 = boolean. + ACTION_RENAME_OBJECT => { + let arg4 = ctx.mem.read_u32(pkt + DP_ARG4); + bool_result(path_arg(ctx.dos, ctx.mem, arg1, arg2).and_then(|old| { + let new = path_arg(ctx.dos, ctx.mem, arg3, arg4)?; + ctx.dos.rename(&old, &new) + })) + } + // ACTION_SET_PROTECT (21): Arg1 unused, Arg2 = parent lock, Arg3 + // = BSTR name, Arg4 = protection mask. Res1 = boolean. + ACTION_SET_PROTECT => { + let mask = ctx.mem.read_u32(pkt + DP_ARG4); + bool_result( + path_arg(ctx.dos, ctx.mem, arg2, arg3) + .and_then(|path| crate::dosprotect::set_protection(ctx.dos, &path, mask)), + ) + } + // ACTION_SET_DATE (34): Arg1 unused, Arg2 = parent lock, Arg3 = + // BSTR name, Arg4 = struct DateStamp*. Res1 = boolean. + ACTION_SET_DATE => { + let date_ptr = ctx.mem.read_u32(pkt + DP_ARG4); + bool_result(path_arg(ctx.dos, ctx.mem, arg2, arg3).and_then(|path| { + crate::dossetfiledate::set_file_date(ctx.dos, ctx.mem, &path, date_ptr) + })) + } + // ACTION_SET_OWNER (1036): Arg1 unused, Arg2 = parent lock, Arg3 + // = BSTR name, Arg4 = owner (UID << 16 | GID). Res1 = boolean. + // Host volumes have no Amiga owner to set, so this succeeds for + // any object that exists, as on a filesystem without + // multi-user support. + ACTION_SET_OWNER => bool_result( + path_arg(ctx.dos, ctx.mem, arg2, arg3).and_then(|path| object_exists(ctx.dos, &path)), + ), + // ACTION_MAKE_LINK (1021): Arg1 = parent lock, Arg2 = BSTR name, + // Arg3 = the target: a lock for a hard link, a C string for a + // soft one, as Arg4 (LINK_HARD/LINK_SOFT) says. Res1 = boolean. + ACTION_MAKE_LINK => { + let soft = ctx.mem.read_u32(pkt + DP_ARG4) == LINK_SOFT; + bool_result(path_arg(ctx.dos, ctx.mem, arg1, arg2).and_then(|path| { + if soft { + let target = + String::from_utf8_lossy(&read_c_string(ctx.mem, arg3)).into_owned(); + ctx.dos.make_link_soft(&path, &target) + } else { + ctx.dos.make_link_hard(&path, addr_from_bptr(arg3)) + } + })) + } + // ACTION_SET_FILE_SIZE (1022): Arg1 = fh_Arg1, Arg2 = offset, + // Arg3 = mode, as for ACTION_SEEK. Res1 = the new size, or -1. + ACTION_SET_FILE_SIZE => match ctx.dos.set_file_size(arg1, arg2 as i32, arg3 as i32) { + Ok(size) => (size as u32, 0), + Err(code) => (RESULT_ERROR, code as u32), + }, + // ACTION_DISK_INFO (25): Arg1 = BPTR to an InfoData. Res1 = + // boolean. The same fixed InfoData as DoPkt's (see the module + // docs). + ACTION_DISK_INFO => { + fill_info_data(ctx.mem, addr_from_bptr(arg1)); + (DOSTRUE, 0) + } + // ACTION_WAIT_CHAR (20): Arg1 = timeout in microseconds. Res1 = + // whether a character arrived; never here, as for the + // WaitForChar call. + ACTION_WAIT_CHAR => (DOSFALSE, 0), + // A handler answers an action it does not know with DOSFALSE: + // RESULT_ERROR (-1) would read as DOSTRUE to a caller of a + // boolean action, which would then believe it succeeded. + _ => (DOSFALSE, ERROR_ACTION_NOT_KNOWN as u32), }; ctx.mem.write_u32(pkt + DP_RES1, res1); @@ -399,7 +529,19 @@ pub fn handle_fs_packet( Ok(()) } -/// Registers `DoPkt` onto [`DOS_LIBRARY_BASE`], looked up by name +/// `Info` (`D1` = lock `BPTR`, `D2` = `struct InfoData*`, longword +/// aligned). `D0` = `DOSTRUE`. Fills the same fixed `InfoData` as +/// `ACTION_DISK_INFO` (see the module docs), whatever volume the lock is +/// on. Found needed running an ixemul.library program (issue #196), +/// which calls it when it resolves a path through the root, `/`. +fn info_handler(ctx: &mut HandlerContext<'_, C>) -> Result<(), DispatchError> { + let info = ctx.cpu.data_register(DataRegister(2)); + fill_info_data(ctx.mem, info); + ctx.cpu.set_data_register(DataRegister(0), DOSTRUE); + Ok(()) +} + +/// Registers `DoPkt` and `Info` onto [`DOS_LIBRARY_BASE`], looked up by name /// through [`DOS_LVOS`]. Called from [`crate::dispatch::Runtime::new`] /// alongside the other `dos.library` registrations. pub fn register_dospkt_handlers( @@ -416,6 +558,16 @@ pub fn register_dospkt_handlers( do_pkt_handler::, ) .unwrap_or_else(|e| panic!("DoPkt should be in DOS_LVOS: {e}")); + table + .register_by_name( + mem, + DOS_LIBRARY_BASE, + DOS_LVOS, + "dos.library", + "Info", + info_handler::, + ) + .unwrap_or_else(|e| panic!("Info should be in DOS_LVOS: {e}")); } #[cfg(test)] @@ -823,10 +975,355 @@ mod tests { "even an unknown action must be replied, not swallowed -- a \ client blocked on the reply port would otherwise hang" ); - assert_eq!(mem.read_u32(pkt + DP_RES1), RESULT_ERROR, "-1, not 0"); + assert_eq!( + mem.read_u32(pkt + DP_RES1), + DOSFALSE, + "0, not -1: -1 is DOSTRUE to a caller of a boolean action" + ); assert_eq!(mem.read_u32(pkt + DP_RES2), ERROR_ACTION_NOT_KNOWN as u32); } + // --- issue #196: the path actions ixemul.library sends --- + + /// One argument of a packet [`run_path_packet`] sends. + enum PktArg { + /// The lock on `SYS:` the guest takes first. + Lock, + /// A BSTR, passed as a `BPTR`. + Bstr(&'static str), + /// A C string, passed as an address. + CStr(&'static str), + /// These bytes, passed as an address. + Bytes(Vec), + /// These bytes, passed as a `BPTR`. + BytesBptr(Vec), + Raw(u32), + } + + /// Locks `SYS:` (`dir`), puts that lock into the packet's + /// [`PktArg::Lock`] arguments and `PutMsg`s the packet to the lock's + /// `fl_Task` -- what ixemul.library's `__plock()` does with the + /// `dvp_Lock` and `dvp_Port` `GetDeviceProc` hands it. Returns + /// `(Res1, Res2, runtime, the address of each argument's data)`. + fn run_path_packet( + dir: &TempDir, + action: u32, + args: Vec, + ) -> (u32, u32, Runtime, Vec) { + let mut words = Vec::new(); + let name_idx = words.len(); + push_move_imm_to_d(&mut words, 1, 0); // D1 = &"SYS:" (patched) + push_move_imm_to_d(&mut words, 2, crate::doslock::SHARED_LOCK as u32); + push_jsr(&mut words, 6, -84); // Lock(a6) -> D0 = BPTR + let mut lock_stores = Vec::new(); + for (i, arg) in args.iter().enumerate() { + if let PktArg::Lock = arg { + lock_stores.push((words.len(), i)); + words.push(0x23C0); // MOVE.L D0,(xxx).L -- dp_ArgN (patched) + words.push(0); + words.push(0); + } + } + words.push(0xE580); // ASL.L #2,D0 (BPTR -> address) + words.push(0x2640); // MOVEA.L D0,A3 + words.push(0x206B); // MOVEA.L (12,A3),A0 -- A0 = fl_Task + words.push(12); + let msg_idx = words.len(); + words.push(0x227C); // MOVEA.L #MSG,A1 (patched) + words.push(0); + words.push(0); + words.push(0x2C7C); // MOVEA.L #EXEC_LIBRARY_BASE,A6 + words.push((crate::dispatch::EXEC_LIBRARY_BASE >> 16) as u16); + words.push(crate::dispatch::EXEC_LIBRARY_BASE as u16); + push_jsr(&mut words, 6, -366); // PutMsg(a6) + let rport_idx = words.len(); + words.push(0x207C); // MOVEA.L #RPORT,A0 (patched) + words.push(0); + words.push(0); + push_jsr(&mut words, 6, -372); // GetMsg(a6) + words.push(RTS); + + let data = (TRAP_TABLE_END + (words.len() as u32) * 2 + 3) & !3; + let (name, pkt, msg, rport) = (data, data + 8, data + 56, data + 80); + let mut free = data + 120; + words[name_idx + 1] = (name >> 16) as u16; + words[name_idx + 2] = name as u16; + for (idx, i) in &lock_stores { + let slot = pkt + DP_ARG1 + 4 * *i as u32; + words[idx + 1] = (slot >> 16) as u16; + words[idx + 2] = slot as u16; + } + words[msg_idx + 1] = (msg >> 16) as u16; + words[msg_idx + 2] = msg as u16; + words[rport_idx + 1] = (rport >> 16) as u16; + words[rport_idx + 2] = rport as u16; + + let mut mem = FlatMemory::new(0x2_0000); + load_words(&mut mem, TRAP_TABLE_END, &words); + crate::guestmem::write_c_string(&mut mem, name, b"SYS:"); + mem.write_u32(msg + crate::execlist::LN_NAME, pkt); + mem.write_u32(pkt + DP_PORT, rport); + mem.write_u32(pkt + DP_TYPE, action); + crate::execlist::init_list_header(&mut mem, rport + crate::execlist::MP_MSGLIST); + let mut addrs = Vec::new(); + for (i, arg) in args.iter().enumerate() { + let slot = pkt + DP_ARG1 + 4 * i as u32; + let at = free; + let (value, bytes): (u32, Vec) = match arg { + PktArg::Lock => (0, Vec::new()), + PktArg::Raw(v) => (*v, Vec::new()), + PktArg::Bstr(s) => { + let mut b = vec![s.len() as u8]; + b.extend_from_slice(s.as_bytes()); + (at >> 2, b) + } + PktArg::CStr(s) => { + let mut b = s.as_bytes().to_vec(); + b.push(0); + (at, b) + } + PktArg::Bytes(b) => (at, b.clone()), + PktArg::BytesBptr(b) => (at >> 2, b.clone()), + }; + for (j, b) in bytes.iter().enumerate() { + mem.write_u8(at + j as u32, *b); + } + free = (free + bytes.len() as u32 + 3) & !3; + addrs.push(at); + if !matches!(arg, PktArg::Lock) { + mem.write_u32(slot, value); + } + } + + let mut rt = Runtime::new( + M68kCpu::new(), + mem, + StartConfig { + entry: TRAP_TABLE_END, + load_end: free + 0x40, + args: Vec::new(), + ..StartConfig::default() + }, + ); + let vfs = Vfs::new(VfsConfig { + volumes: vec![("SYS".to_string(), dir.path.clone())], + assigns: vec![], + auto_assign_root: None, + cwd: "SYS:".to_string(), + ..Default::default() + }) + .expect("build vfs"); + rt.set_vfs(vfs); + let mut out = Vec::new(); + let code = rt.run(&mut out, None).expect("run should succeed"); + assert_eq!(code as u32, msg, "the packet must be replied"); + let mem = rt.memory(); + let (res1, res2) = (mem.read_u32(pkt + DP_RES1), mem.read_u32(pkt + DP_RES2)); + (res1, res2, rt, addrs) + } + + #[test] + fn action_create_dir_creates_the_directory_and_returns_a_lock() { + let dir = TempDir::new("pkt-createdir"); + let (res1, res2, _, _) = run_path_packet( + &dir, + ACTION_CREATE_DIR, + vec![PktArg::Lock, PktArg::Bstr("newdir")], + ); + assert_ne!(res1, 0, "Res1 = a lock on the new directory"); + assert_eq!(res2, 0); + assert!(dir.path.join("newdir").is_dir()); + } + + #[test] + fn action_delete_object_deletes_and_reports_a_missing_object() { + let dir = TempDir::new("pkt-delete"); + std::fs::write(dir.path.join("f.txt"), b"x").unwrap(); + let (res1, _, _, _) = run_path_packet( + &dir, + ACTION_DELETE_OBJECT, + vec![PktArg::Lock, PktArg::Bstr("f.txt")], + ); + assert_eq!(res1, DOSTRUE); + assert!(!dir.path.join("f.txt").exists()); + + let (res1, res2, _, _) = run_path_packet( + &dir, + ACTION_DELETE_OBJECT, + vec![PktArg::Lock, PktArg::Bstr("f.txt")], + ); + assert_eq!(res1, DOSFALSE); + assert_eq!(res2, ERROR_OBJECT_NOT_FOUND as u32); + } + + #[test] + fn action_rename_object_renames() { + let dir = TempDir::new("pkt-rename"); + std::fs::write(dir.path.join("a"), b"x").unwrap(); + let (res1, _, _, _) = run_path_packet( + &dir, + ACTION_RENAME_OBJECT, + vec![ + PktArg::Lock, + PktArg::Bstr("a"), + PktArg::Lock, + PktArg::Bstr("b"), + ], + ); + assert_eq!(res1, DOSTRUE); + assert!(!dir.path.join("a").exists()); + assert_eq!(std::fs::read(dir.path.join("b")).unwrap(), b"x"); + } + + #[test] + fn action_set_protect_write_protects() { + let dir = TempDir::new("pkt-protect"); + let f = dir.path.join("f.txt"); + std::fs::write(&f, b"x").unwrap(); + const FIBF_WRITE: u32 = 1 << 2; + let (res1, _, _, _) = run_path_packet( + &dir, + ACTION_SET_PROTECT, + vec![ + PktArg::Raw(0), + PktArg::Lock, + PktArg::Bstr("f.txt"), + PktArg::Raw(FIBF_WRITE), + ], + ); + assert_eq!(res1, DOSTRUE); + let readonly = std::fs::metadata(&f).unwrap().permissions().readonly(); + let mut perms = std::fs::metadata(&f).unwrap().permissions(); + #[allow(clippy::permissions_set_readonly_false)] + perms.set_readonly(false); + std::fs::set_permissions(&f, perms).unwrap(); + assert!(readonly); + } + + #[test] + fn action_set_date_records_the_datestamp() { + let dir = TempDir::new("pkt-setdate"); + std::fs::write(dir.path.join("f.txt"), b"x").unwrap(); + let mut ds = Vec::new(); + for v in [8000u32, 600, 25] { + ds.extend_from_slice(&v.to_be_bytes()); + } + let (res1, _, _, _) = run_path_packet( + &dir, + ACTION_SET_DATE, + vec![ + PktArg::Raw(0), + PktArg::Lock, + PktArg::Bstr("f.txt"), + PktArg::Bytes(ds), + ], + ); + assert_eq!(res1, DOSTRUE); + let meta = crate::dosmeta::read_sidecar(&dir.path.join("f.txt")).unwrap(); + assert_eq!(meta.date, (8000, 600, 25)); + } + + #[test] + fn action_set_owner_succeeds_only_for_an_existing_object() { + let dir = TempDir::new("pkt-setowner"); + std::fs::write(dir.path.join("f.txt"), b"x").unwrap(); + let (res1, _, _, _) = run_path_packet( + &dir, + ACTION_SET_OWNER, + vec![ + PktArg::Raw(0), + PktArg::Lock, + PktArg::Bstr("f.txt"), + PktArg::Raw(0x0001_0002), + ], + ); + assert_eq!(res1, DOSTRUE); + let (res1, res2, _, _) = run_path_packet( + &dir, + ACTION_SET_OWNER, + vec![ + PktArg::Raw(0), + PktArg::Lock, + PktArg::Bstr("missing"), + PktArg::Raw(0), + ], + ); + assert_eq!(res1, DOSFALSE); + assert_eq!(res2, ERROR_OBJECT_NOT_FOUND as u32); + } + + #[test] + fn action_make_link_soft_creates_a_link() { + let dir = TempDir::new("pkt-makelink"); + std::fs::write(dir.path.join("f.txt"), b"x").unwrap(); + let (res1, res2, _, _) = run_path_packet( + &dir, + ACTION_MAKE_LINK, + vec![ + PktArg::Lock, + PktArg::Bstr("ln"), + PktArg::CStr("f.txt"), + PktArg::Raw(LINK_SOFT), + ], + ); + assert_eq!((res1, res2), (DOSTRUE, 0)); + assert!(std::fs::symlink_metadata(dir.path.join("ln")).is_ok()); + } + + #[test] + fn action_disk_info_fills_the_infodata() { + let dir = TempDir::new("pkt-diskinfo"); + let (res1, _, rt, addrs) = + run_path_packet(&dir, ACTION_DISK_INFO, vec![PktArg::BytesBptr(vec![0; 36])]); + assert_eq!(res1, DOSTRUE); + let info = addrs[0]; + assert_eq!( + rt.memory().read_u32(info + ID_BYTESPERBLOCK_OFFSET), + BYTES_PER_BLOCK + ); + assert_eq!(rt.memory().read_u32(info + ID_DISKTYPE_OFFSET), ID_DOS_DISK); + } + + #[test] + fn info_fills_the_infodata_and_returns_dostrue() { + let mut words = Vec::new(); + push_move_imm_to_d(&mut words, 1, 0); // D1 = lock (0: current dir) + let buf_idx = words.len(); + push_move_imm_to_d(&mut words, 2, 0); // D2 = &InfoData (patched) + push_jsr(&mut words, 6, -114); // Info(a6) + words.push(RTS); + let buf = (TRAP_TABLE_END + (words.len() as u32) * 2 + 3) & !3; + words[buf_idx + 1] = (buf >> 16) as u16; + words[buf_idx + 2] = buf as u16; + let mut mem = FlatMemory::new(0x2_0000); + load_words(&mut mem, TRAP_TABLE_END, &words); + let mut rt = Runtime::new( + M68kCpu::new(), + mem, + StartConfig { + entry: TRAP_TABLE_END, + load_end: buf + 0x40, + args: Vec::new(), + ..StartConfig::default() + }, + ); + let mut out = Vec::new(); + let code = rt.run(&mut out, None).expect("run should succeed"); + assert_eq!(code as u32, DOSTRUE); + assert_eq!( + rt.memory().read_u32(buf + ID_BYTESPERBLOCK_OFFSET), + BYTES_PER_BLOCK + ); + } + + #[test] + fn action_wait_char_reports_no_character() { + let dir = TempDir::new("pkt-waitchar"); + let (res1, res2, _, _) = + run_path_packet(&dir, ACTION_WAIT_CHAR, vec![PktArg::Raw(1_000_000)]); + assert_eq!((res1, res2), (DOSFALSE, 0)); + } + // --- issue #186: AllocDosObject(DOS_FILEHANDLE) + packet-level // ACTION_LOCATE_OBJECT/ACTION_FINDOUTPUT, reproducing the real // ixemul.library call sequence as closely as this test harness can diff --git a/crates/volamos-core/src/dosprotect.rs b/crates/volamos-core/src/dosprotect.rs index 563147f..405f6ef 100644 --- a/crates/volamos-core/src/dosprotect.rs +++ b/crates/volamos-core/src/dosprotect.rs @@ -28,7 +28,7 @@ use crate::cpu::{Cpu, DataRegister}; use crate::dispatch::{DOS_LIBRARY_BASE, DispatchError, HandlerContext, LibraryTable}; -use crate::dosfile::map_io_error; +use crate::dosfile::{DosState, map_io_error}; use crate::dosmeta; use crate::guestmem::read_c_string; use crate::lvos::dos::DOS_LVOS; @@ -42,6 +42,31 @@ const DOSFALSE: u32 = 0; /// system refuses to modify the file" (write-protected). const FIBF_WRITE: u32 = 1 << 2; +/// `SetProtection`'s work on `name` (resolved like the library call's), +/// shared with the `ACTION_SET_PROTECT` packet ([`crate::dospkt`]). +pub(crate) fn set_protection(dos: &DosState, name: &str, mask: u32) -> Result<(), i32> { + let vfs = dos + .vfs + .as_ref() + .ok_or(crate::dosfile::ERROR_OBJECT_NOT_FOUND)?; + let host_path = vfs + .resolve(name, ResolveMode::MustExist) + .map_err(|e| crate::dosfile::map_vfs_error(&e))?; + let mut perms = fs::metadata(&host_path) + .map_err(|e| map_io_error(&e))? + .permissions(); + perms.set_readonly(mask & FIBF_WRITE != 0); + fs::set_permissions(&host_path, perms).map_err(|e| map_io_error(&e))?; + + let mut sidecar_meta = dosmeta::read_sidecar(&host_path).unwrap_or_default(); + sidecar_meta.prot = mask; + // Best-effort: a sidecar write failure shouldn't fail the whole + // call when the real host permission change (the primary, + // always-real effect) already succeeded above. + let _ = dosmeta::write_sidecar(&host_path, &sidecar_meta); + Ok(()) +} + /// `SetProtection` (`D1` = name `CString*`, `D2` = protection mask). /// `D0` = `DOSTRUE`/`DOSFALSE` (+ `IoErr()` set on failure). fn set_protection_handler(ctx: &mut HandlerContext<'_, C>) -> Result<(), DispatchError> { @@ -49,31 +74,7 @@ fn set_protection_handler(ctx: &mut HandlerContext<'_, C>) -> Result<(), let mask = ctx.cpu.data_register(DataRegister(2)); let name = String::from_utf8_lossy(&read_c_string(ctx.mem, name_ptr)).into_owned(); - let result = (|| -> Result<(), i32> { - let vfs = ctx - .dos - .vfs - .as_ref() - .ok_or(crate::dosfile::ERROR_OBJECT_NOT_FOUND)?; - let host_path = vfs - .resolve(&name, ResolveMode::MustExist) - .map_err(|e| crate::dosfile::map_vfs_error(&e))?; - let mut perms = fs::metadata(&host_path) - .map_err(|e| map_io_error(&e))? - .permissions(); - perms.set_readonly(mask & FIBF_WRITE != 0); - fs::set_permissions(&host_path, perms).map_err(|e| map_io_error(&e))?; - - let mut sidecar_meta = dosmeta::read_sidecar(&host_path).unwrap_or_default(); - sidecar_meta.prot = mask; - // Best-effort: a sidecar write failure shouldn't fail the whole - // call when the real host permission change (the primary, - // always-real effect) already succeeded above. - let _ = dosmeta::write_sidecar(&host_path, &sidecar_meta); - Ok(()) - })(); - - match result { + match set_protection(ctx.dos, &name, mask) { Ok(()) => ctx.cpu.set_data_register(DataRegister(0), DOSTRUE), Err(code) => { ctx.dos.set_io_err(code); diff --git a/crates/volamos-core/src/dossetfiledate.rs b/crates/volamos-core/src/dossetfiledate.rs index 2307191..412f22e 100644 --- a/crates/volamos-core/src/dossetfiledate.rs +++ b/crates/volamos-core/src/dossetfiledate.rs @@ -16,7 +16,7 @@ use crate::cpu::{Cpu, DataRegister}; use crate::dispatch::{DOS_LIBRARY_BASE, DispatchError, HandlerContext, LibraryTable}; -use crate::dosfile::map_io_error; +use crate::dosfile::{DosState, map_io_error}; use crate::dosmeta; use crate::guestmem::read_c_string; use crate::lvos::dos::DOS_LVOS; @@ -30,36 +30,44 @@ const DS_DAYS_OFFSET: u32 = 0; const DS_MINUTE_OFFSET: u32 = 4; const DS_TICK_OFFSET: u32 = 8; +/// `SetFileDate`'s work on `name` (resolved like the library call's), +/// with the `struct DateStamp` at `date_ptr`; shared with the +/// `ACTION_SET_DATE` packet ([`crate::dospkt`]). +pub(crate) fn set_file_date( + dos: &DosState, + mem: &dyn AddressSpace, + name: &str, + date_ptr: u32, +) -> Result<(), i32> { + let days = mem.read_u32(date_ptr.wrapping_add(DS_DAYS_OFFSET)) as i32; + let minute = mem.read_u32(date_ptr.wrapping_add(DS_MINUTE_OFFSET)) as i32; + let tick = mem.read_u32(date_ptr.wrapping_add(DS_TICK_OFFSET)) as i32; + + let vfs = dos + .vfs + .as_ref() + .ok_or(crate::dosfile::ERROR_OBJECT_NOT_FOUND)?; + let host_path = vfs + .resolve(name, ResolveMode::MustExist) + .map_err(|e| crate::dosfile::map_vfs_error(&e))?; + // Touch the target so a missing object fails cleanly + // (SetFileDate has no other host-side effect to fail on + // otherwise -- same convention as crate::dosnote's SetComment). + std::fs::metadata(&host_path).map_err(|e| map_io_error(&e))?; + + let mut sidecar_meta = dosmeta::read_sidecar(&host_path).unwrap_or_default(); + sidecar_meta.date = (days, minute, tick); + dosmeta::write_sidecar(&host_path, &sidecar_meta).map_err(|e| map_io_error(&e)) +} + /// `SetFileDate` (`D1` = name `CString*`, `D2` = `struct DateStamp*`). /// `D0` = `DOSTRUE`/`DOSFALSE` (+ `IoErr()` set on failure). fn set_file_date_handler(ctx: &mut HandlerContext<'_, C>) -> Result<(), DispatchError> { let name_ptr = ctx.cpu.data_register(DataRegister(1)); let date_ptr = ctx.cpu.data_register(DataRegister(2)); let name = String::from_utf8_lossy(&read_c_string(ctx.mem, name_ptr)).into_owned(); - let days = ctx.mem.read_u32(date_ptr.wrapping_add(DS_DAYS_OFFSET)) as i32; - let minute = ctx.mem.read_u32(date_ptr.wrapping_add(DS_MINUTE_OFFSET)) as i32; - let tick = ctx.mem.read_u32(date_ptr.wrapping_add(DS_TICK_OFFSET)) as i32; - - let result = (|| -> Result<(), i32> { - let vfs = ctx - .dos - .vfs - .as_ref() - .ok_or(crate::dosfile::ERROR_OBJECT_NOT_FOUND)?; - let host_path = vfs - .resolve(&name, ResolveMode::MustExist) - .map_err(|e| crate::dosfile::map_vfs_error(&e))?; - // Touch the target so a missing object fails cleanly - // (SetFileDate has no other host-side effect to fail on - // otherwise -- same convention as crate::dosnote's SetComment). - std::fs::metadata(&host_path).map_err(|e| map_io_error(&e))?; - - let mut sidecar_meta = dosmeta::read_sidecar(&host_path).unwrap_or_default(); - sidecar_meta.date = (days, minute, tick); - dosmeta::write_sidecar(&host_path, &sidecar_meta).map_err(|e| map_io_error(&e)) - })(); - - match result { + + match set_file_date(ctx.dos, ctx.mem, &name, date_ptr) { Ok(()) => ctx.cpu.set_data_register(DataRegister(0), DOSTRUE), Err(code) => { ctx.dos.set_io_err(code);