Skip to content

Commit 81e6d23

Browse files
authored
Fix set_len, read, write, and sync_all bugs (#764)
* Fix set_len to keep position * Clean up formatting * Rename orig to cursor * Fix reads and writes * Fix sync bugs
1 parent d7af8e1 commit 81e6d23

File tree

2 files changed

+143
-4
lines changed

2 files changed

+143
-4
lines changed

crates/vfs/src/memory/file.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,19 @@ impl File {
114114
/// Attempts to sync all OS-internal metadata to disk.
115115
#[allow(dead_code)]
116116
pub async fn sync_all(&self) -> io::Result<()> {
117+
// Lock the Inner state and drive it to Idle
118+
let mut inner = self.inner.lock().await;
119+
inner.complete_inflight().await;
117120
Ok(())
118121
}
119122

120123
/// This function is similar to `sync_all`, except that it may not
121124
/// synchronize file metadata to the filesystem.
122125
#[allow(dead_code)]
123126
pub async fn sync_data(&self) -> io::Result<()> {
127+
// Lock the Inner state and drive it to Idle
128+
let mut inner = self.inner.lock().await;
129+
inner.complete_inflight().await;
124130
Ok(())
125131
}
126132

@@ -133,6 +139,8 @@ impl File {
133139
/// will be extended to size and have all of the
134140
/// intermediate data filled in with 0s.
135141
///
142+
/// Length changes to not affect the cursor position.
143+
///
136144
/// # Errors
137145
///
138146
/// This function will return an error if the file
@@ -157,6 +165,7 @@ impl File {
157165
inner.state = Busy(Box::pin(async move {
158166
let mut std = std.lock();
159167
let len = std.get_ref().len() as u64;
168+
let cursor_pos = std.position();
160169

161170
let extension = if size <= len {
162171
None
@@ -182,9 +191,9 @@ impl File {
182191
}
183192
Ok(())
184193
}
185-
.map(|_| 0); // the value is discarded later
194+
.map(|_| cursor_pos);
186195

187-
// Return the result as a seek
196+
// Reset to the original position (even if it's > size now)
188197
(Operation::Seek(res), buf)
189198
}));
190199

@@ -241,7 +250,8 @@ impl AsyncRead for File {
241250
let mut buf = buf_cell.take().unwrap();
242251

243252
if !buf.is_empty() {
244-
buf.copy_to(dst);
253+
let n = buf.copy_to(dst);
254+
inner.pos += n as u64;
245255
*buf_cell = Some(buf);
246256
return Ready(Ok(()));
247257
}
@@ -260,7 +270,8 @@ impl AsyncRead for File {
260270

261271
match op {
262272
Operation::Read(Ok(_)) => {
263-
buf.copy_to(dst);
273+
let n = buf.copy_to(dst);
274+
inner.pos += n as u64;
264275
inner.state = Idle(Some(buf));
265276
return Ready(Ok(()));
266277
}
@@ -388,6 +399,9 @@ impl AsyncWrite for File {
388399

389400
let n = buf.copy_from(src);
390401

402+
// Update position tracking with the number of bytes written
403+
inner.pos += n as u64;
404+
391405
let std = me.std.clone();
392406

393407
let blocking_task_join_handle = Box::pin(async move {

crates/vfs/src/tests.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,131 @@ mod tests {
6060
Ok(())
6161
}
6262

63+
#[tokio::test]
64+
async fn poll_complete_after_write() -> Result<()> {
65+
use futures::future::poll_fn;
66+
use std::pin::Pin;
67+
use tokio::io::{AsyncSeek, AsyncWriteExt};
68+
69+
let path = "pos_after_write.txt";
70+
let mut f = File::create(path).await?;
71+
f.write_all(b"hello").await?; // 5 bytes
72+
f.flush().await?; // wait for the write to finish
73+
74+
// Ask the file itself where it thinks the cursor is.
75+
let mut pinned = Pin::new(&mut f);
76+
let pos = poll_fn(|cx| pinned.as_mut().poll_complete(cx)).await?;
77+
78+
// **BUG**: returns 0 with the current implementation
79+
assert_eq!(pos, 5, "cursor should sit just past the 5 written bytes");
80+
Ok(())
81+
}
82+
83+
#[tokio::test]
84+
async fn poll_complete_after_set_len() -> Result<()> {
85+
use futures::future::poll_fn;
86+
use std::pin::Pin;
87+
use tokio::io::{AsyncSeek, AsyncSeekExt, AsyncWriteExt};
88+
89+
let path = "pos_after_set_len.txt";
90+
let mut f = File::create(path).await?;
91+
f.write_all(b"abcdef").await?; // 6 bytes
92+
f.flush().await?;
93+
f.seek(std::io::SeekFrom::Start(2)).await?; // move to byte 2
94+
95+
f.set_len(100).await?; // extend – should *not* move the cursor
96+
97+
let mut pinned = Pin::new(&mut f);
98+
let pos = poll_fn(|cx| pinned.as_mut().poll_complete(cx)).await?;
99+
100+
// **BUG**: current code reports 0 instead of 2
101+
assert_eq!(pos, 2, "set_len must preserve the current offset");
102+
Ok(())
103+
}
104+
105+
#[tokio::test]
106+
async fn poll_complete_after_read() -> Result<()> {
107+
use futures::future::poll_fn;
108+
use std::pin::Pin;
109+
use tokio::io::{AsyncReadExt, AsyncSeek, AsyncWriteExt};
110+
111+
// Create a test file with some content
112+
let path = "pos_after_read.txt";
113+
let mut f = File::create(path).await?;
114+
f.write_all(b"hello world").await?; // 11 bytes
115+
f.flush().await?;
116+
117+
// Close and reopen the file for reading
118+
drop(f);
119+
let mut f = File::open(path).await?;
120+
121+
// Read part of the content
122+
let mut buf = [0u8; 5]; // Only read first 5 bytes
123+
f.read_exact(&mut buf).await?;
124+
assert_eq!(&buf, b"hello");
125+
126+
// Ask the file where it thinks the cursor is
127+
let mut pinned = Pin::new(&mut f);
128+
let pos = poll_fn(|cx| pinned.as_mut().poll_complete(cx)).await?;
129+
130+
// Verify the cursor has advanced by the number of bytes read
131+
assert_eq!(pos, 5, "cursor should sit just past the 5 read bytes");
132+
133+
// Read more bytes and check position again
134+
f.read_exact(&mut buf).await?; // Read next 5 bytes
135+
assert_eq!(&buf, b" worl");
136+
137+
let mut pinned = Pin::new(&mut f);
138+
let pos = poll_fn(|cx| pinned.as_mut().poll_complete(cx)).await?;
139+
140+
// Verify cursor has advanced further
141+
assert_eq!(pos, 10, "cursor should advance after additional reads");
142+
143+
Ok(())
144+
}
145+
146+
#[tokio::test]
147+
async fn seek_after_pending_write() -> Result<()> {
148+
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
149+
150+
let path = "seek_after_pending.txt";
151+
let mut f = File::create(path).await?;
152+
f.write_all(b"xyz").await?; // leaves an in-flight write
153+
154+
// Try to find out where we are without an explicit flush.
155+
// Tokio’s real File lets this succeed; our version currently errors.
156+
let pos = f.seek(std::io::SeekFrom::Current(0)).await;
157+
158+
// **BUG**: expect Ok(3), get Err(other operation is pending)
159+
assert_eq!(
160+
pos.unwrap(),
161+
3,
162+
"seek should wait for the write to finish"
163+
);
164+
Ok(())
165+
}
166+
167+
#[tokio::test]
168+
async fn test_sync_all() -> Result<()> {
169+
let buf = [0u8; 1024];
170+
let path = "test_sync_all.txt";
171+
let tmp_path = "test_sync_all.txt.tmp";
172+
let mut f = OpenOptions::new()
173+
.write(true)
174+
.create(true)
175+
.truncate(true)
176+
.open(&tmp_path)
177+
.await?;
178+
f.write_all(&buf).await?;
179+
f.sync_all().await?;
180+
assert!(vfs::try_exists(&tmp_path).await?);
181+
assert_eq!(vfs::metadata(&tmp_path).await?.len(), buf.len() as u64);
182+
vfs::rename(tmp_path, path).await?;
183+
assert!(vfs::try_exists(path).await?);
184+
assert_eq!(vfs::metadata(path).await?.len(), buf.len() as u64);
185+
Ok(())
186+
}
187+
63188
async fn file_write_read() -> Result<()> {
64189
let path = "test.txt";
65190
let contents = "Mock content";

0 commit comments

Comments
 (0)