FTP connections preserve passwords and filenames, with CI checking every change #1

Merged
temeddix merged 15 commits from fix-directory-listing into main 2026-09-30 13:10:40 +00:00
3 changed files with 34 additions and 2 deletions
Showing only changes of commit 25d572aa33 - Show all commits
+6 -2
View File
1
@@ -100,8 +100,12 @@ fn with_control<T>(
/// idle session is replaced and logged in again. Nothing inside a transfer or
/// mutation retries, because its missing reply may hide a completed change.
fn ready() -> Result<(), ErrorCode> {
if with_control(|control| control.expect("NOOP")).is_ok() {
return Ok(());
if !CONTROL.with_borrow(Option::is_none) {
Review

Propagate Cancelled (and other non-recoverable preflight errors) instead of reconnecting: a cancelled NOOP currently falls through to session.connect() and then executes the requested operation, which can perform a mutation after the host cancelled the invocation. Retry only the disconnect error(s) that are safe to recover from.

Profile: codex · gpt-5.6-terra

Propagate `Cancelled` (and other non-recoverable preflight errors) instead of reconnecting: a cancelled `NOOP` currently falls through to `session.connect()` and then executes the requested operation, which can perform a mutation after the host cancelled the invocation. Retry only the disconnect error(s) that are safe to recover from. > Profile: `codex` · `gpt-5.6-terra`
match with_control(|control| control.expect("NOOP")) {
Ok(_) => return Ok(()),
Err(ErrorCode::Unavailable) => {}
Err(error) => return Err(error),
}
}
let session = SESSION
.with_borrow(Clone::clone)
3
+17
View File
@@ -58,3 +58,20 @@ async fn recovery_before_a_mutation_never_replays_a_lost_mutation_reply()
assert_eq!(summary.mkdir_commands, 1);
Ok(())
}
#[tokio::test]
async fn cancelled_preflight_never_reconnects_or_executes_the_mutation()
-> TestResult {
let fixture = test_host::install(test_host::Scenario::CancelledPreflight);
guest(Ftp::open("ftp".into(), config()))?;
assert!(matches!(
Ftp::mkdir(Locator::Path("/cancelled".into())),
Err(ErrorCode::Cancelled)
));
Ftp::close();
let summary = fixture.summary();
assert_eq!(summary.control_connections, 1);
assert_eq!(summary.listings, 0);
assert_eq!(summary.mkdir_commands, 0);
Ok(())
}
+11
View File
@@ -13,6 +13,7 @@ const LISTING: &[u8] = b"type=file;size=3; report.md \r\n";
#[derive(Clone, Copy)]
pub enum Scenario {
CancelledPreflight,
ExpiredListing,
LostMutationReply,
}
@@ -142,6 +143,9 @@ impl Tcp {
state.listings += 1;
"150 Listing\r\n226 Complete\r\n"
}
"NOOP" if matches!(state.scenario, Scenario::CancelledPreflight) => {
return Err(HostError::Cancelled);
}
"NOOP"
if index == 0
&& (matches!(state.scenario, Scenario::LostMutationReply)
@@ -157,6 +161,13 @@ impl Tcp {
state.mkdir_commands += 1;
return Ok(bytes.len() as u32);
}
"MKD /cancelled"
if index == 1
&& matches!(state.scenario, Scenario::CancelledPreflight) =>
{
state.mkdir_commands += 1;
return Err(HostError::Unavailable);
}
_ => return Err(HostError::InvalidInput),
}
};