Discard uncertain FTP upload sessions
This commit is contained in:
+26
-6
@@ -259,17 +259,37 @@ struct FtpWriter {
|
|||||||
written: Cell<u64>,
|
written: Cell<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FtpWriter {
|
||||||
|
/// A failed data write can leave both the remote file and its pending
|
||||||
|
/// completion reply in an unknown state. Close both channels so the next
|
||||||
|
/// filesystem operation must start a fresh session.
|
||||||
|
fn discard_uncertain_upload(&self) -> ErrorCode {
|
||||||
|
drop(self.socket.take());
|
||||||
|
CONTROL.set(None);
|
||||||
|
ErrorCode::OutcomeUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl GuestWriter for FtpWriter {
|
impl GuestWriter for FtpWriter {
|
||||||
fn push(&self, data: Vec<u8>) -> Result<(), ErrorCode> {
|
fn push(&self, data: Vec<u8>) -> Result<(), ErrorCode> {
|
||||||
let socket = self.socket.borrow();
|
if self.socket.borrow().is_none() {
|
||||||
let socket = socket.as_ref().ok_or(ErrorCode::InvalidInput)?;
|
return Err(ErrorCode::InvalidInput);
|
||||||
|
}
|
||||||
let mut sent = 0;
|
let mut sent = 0;
|
||||||
while sent < data.len() {
|
while sent < data.len() {
|
||||||
let written = socket
|
let result = {
|
||||||
.write(&data[sent..])
|
let socket = self.socket.borrow();
|
||||||
.map_err(|_| ErrorCode::OutcomeUnknown)? as usize;
|
let Some(socket) = socket.as_ref() else {
|
||||||
|
return Err(ErrorCode::InvalidInput);
|
||||||
|
};
|
||||||
|
socket.write(&data[sent..])
|
||||||
|
};
|
||||||
|
let written = match result {
|
||||||
|
Ok(written) => written as usize,
|
||||||
|
Err(_) => return Err(self.discard_uncertain_upload()),
|
||||||
|
};
|
||||||
if written == 0 {
|
if written == 0 {
|
||||||
return Err(ErrorCode::OutcomeUnknown);
|
return Err(self.discard_uncertain_upload());
|
||||||
}
|
}
|
||||||
sent += written;
|
sent += written;
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-2
@@ -1,8 +1,9 @@
|
|||||||
//! FTP command recovery against an in-memory protocol transport. Live QA
|
//! FTP command recovery against an in-memory protocol transport. Live QA
|
||||||
//! separately exercises the compiled WASM component over real host sockets.
|
//! separately exercises the compiled WASM component over real host sockets.
|
||||||
|
|
||||||
use super::{Ftp, mutation_transfer, settle_mutation, test_host};
|
use super::{Ftp, FtpWriter, mutation_transfer, settle_mutation, test_host};
|
||||||
use memona_plugin_sdk::{ErrorCode, Guest, Locator, Setting};
|
use memona_plugin_sdk::{ErrorCode, Guest, GuestWriter, Locator, Setting};
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
|
||||||
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
|
||||||
@@ -91,3 +92,29 @@ async fn a_lost_upload_completion_reports_an_unknown_outcome() -> TestResult {
|
|||||||
assert_eq!(summary.uploaded_bytes, 4);
|
assert_eq!(summary.uploaded_bytes, 4);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_uncertain_upload_write_discards_the_control_session() -> TestResult
|
||||||
|
{
|
||||||
|
let fixture = test_host::install(test_host::Scenario::LostUploadData);
|
||||||
|
guest(Ftp::open("ftp".into(), config()))?;
|
||||||
|
let writer = FtpWriter {
|
||||||
|
path: "/broken".into(),
|
||||||
|
socket: RefCell::new(Some(guest(mutation_transfer("STOR /broken"))?)),
|
||||||
|
written: Cell::new(0),
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
writer.push(b"body".to_vec()),
|
||||||
|
Err(ErrorCode::OutcomeUnknown)
|
||||||
|
));
|
||||||
|
let page = guest(Ftp::list(Locator::Path("/".into()), None, 10))?;
|
||||||
|
assert_eq!(page.entries[0].name, " report.md ");
|
||||||
|
drop(writer);
|
||||||
|
Ftp::close();
|
||||||
|
let summary = fixture.summary();
|
||||||
|
assert_eq!(summary.control_connections, 2);
|
||||||
|
assert_eq!(summary.upload_commands, 1);
|
||||||
|
assert_eq!(summary.uploaded_bytes, 0);
|
||||||
|
assert_eq!(summary.listings, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
+13
-1
@@ -15,6 +15,7 @@ const LISTING: &[u8] = b"type=file;size=3; report.md \r\n";
|
|||||||
pub enum Scenario {
|
pub enum Scenario {
|
||||||
CancelledPreflight,
|
CancelledPreflight,
|
||||||
ExpiredListing,
|
ExpiredListing,
|
||||||
|
LostUploadData,
|
||||||
LostMutationReply,
|
LostMutationReply,
|
||||||
LostUploadCompletion,
|
LostUploadCompletion,
|
||||||
}
|
}
|
||||||
@@ -156,7 +157,12 @@ impl Tcp {
|
|||||||
"TYPE I" => "200 Binary\r\n",
|
"TYPE I" => "200 Binary\r\n",
|
||||||
"QUIT" => "221 Bye\r\n",
|
"QUIT" => "221 Bye\r\n",
|
||||||
"EPSV" => "229 Passive (|||31310|)\r\n",
|
"EPSV" => "229 Passive (|||31310|)\r\n",
|
||||||
"MLSD /" if matches!(state.scenario, Scenario::ExpiredListing) => {
|
"MLSD /"
|
||||||
|
if matches!(
|
||||||
|
state.scenario,
|
||||||
|
Scenario::ExpiredListing | Scenario::LostUploadData
|
||||||
|
) =>
|
||||||
|
{
|
||||||
state.listings += 1;
|
state.listings += 1;
|
||||||
"150 Listing\r\n226 Complete\r\n"
|
"150 Listing\r\n226 Complete\r\n"
|
||||||
}
|
}
|
||||||
@@ -177,6 +183,12 @@ impl Tcp {
|
|||||||
state.upload_commands += 1;
|
state.upload_commands += 1;
|
||||||
"150 Upload started\r\n"
|
"150 Upload started\r\n"
|
||||||
}
|
}
|
||||||
|
"STOR /broken"
|
||||||
|
if matches!(state.scenario, Scenario::LostUploadData) =>
|
||||||
|
{
|
||||||
|
state.upload_commands += 1;
|
||||||
|
"150 Upload started\r\n"
|
||||||
|
}
|
||||||
"MKD /new"
|
"MKD /new"
|
||||||
if index == 1
|
if index == 1
|
||||||
&& matches!(state.scenario, Scenario::LostMutationReply) =>
|
&& matches!(state.scenario, Scenario::LostMutationReply) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user