fix(io): survive locked, shrinking and cloud-placeholder drawings

Opening a drawing that another program or machine holds without read
sharing used to fail with a raw OS error, and the memory-mapped fast
path could kill the process outright: a mapped file that shrinks
mid-parse (a writer saving over it) or a cloud placeholder that cannot
page in raises an in-page exception no Rust code can catch.

Read the DWG through a guarded fast path instead: mmap when possible,
and fall back to one in-memory snapshot read — opened with the most
permissive share mode Windows offers (FILE_SHARE_READ | WRITE |
DELETE) — whenever the map fails or an I/O error surfaces mid-parse.
A snapshot can never fault: a concurrent writer just ends the read
early. Sharing violations (os error 32/33) now answer with a message
that names the file and the remedy, and OneDrive-style placeholder
attributes skip mmap entirely.
This commit is contained in:
huanld committed 2026-09-19 16:52:24 +07:00
1 parent f8d5099cda
commit dc23d4073a
1 file changed
+126 -15
+126 -15
View File
@@ -318,10 +318,24 @@ async fn open_path_with_phase_attempt(
});
callback
};
std::fs::File::open(&path2).map_err(|error| OpenAttemptFailure {
message: format!("failed to open drawing: {error}"),
read_stats: None,
recoverable: false,
std::fs::File::open(&path2).map_err(|error| {
// 32 = ERROR_SHARING_VIOLATION, 33 = ERROR_LOCK_VIOLATION:
// another program holds the drawing without read sharing,
// so nothing on the machine can read the bytes until it
// closes the file — not even read-only.
let message = match error.raw_os_error() {
Some(32) | Some(33) => format!(
"\"{}\" is in use by another program. Close the file there and \
reopen it here, or open a copy of the file.",
path2.display()
),
_ => format!("failed to open drawing: {error}"),
};
OpenAttemptFailure {
message,
read_stats: None,
recoverable: false,
}
})?;
let outcome = load_file_for_open(&path2, Some(parser_progress), &attempt)?;
let read_stats = outcome.stats;
@@ -1035,20 +1049,117 @@ fn read_dwg_path(
DwgReadOptions::default()
};
#[cfg(not(target_arch = "wasm32"))]
let mut reader = {
let mut reader = DwgReader::from_mmap(path).map_err(ReaderFailure::from_reader)?;
reader.options = options;
{
// The memory-mapped read is the fast path, but a mapped file is read
// through the page-fault handler: a file that shrinks while mapped
// (a writer saving over it) or a cloud placeholder that cannot page
// in raises an in-page exception the process cannot catch. Both are
// ordinary I/O errors through plain reads, so anything that can
// fault — or refuses to open under a third-party lock — falls back
// to one in-memory snapshot read below.
if !cloud_placeholder(path) {
match DwgReader::from_mmap(path) {
Ok(mut reader) => {
reader.options = options.clone();
if let Some(progress) = &progress {
reader.set_progress_callback(progress.clone());
}
match reader.read_with_stats() {
Ok(outcome) => return Ok(outcome),
// The file shrank or became unreadable mid-parse;
// retry from a snapshot instead of reporting a bare
// I/O error.
Err(acadrust::DxfError::Io(_)) => {}
Err(error) => return Err(ReaderFailure::from_reader(error)),
}
}
// Locked or unmappable: the snapshot read surfaces the real
// error when the file genuinely cannot be read.
Err(_) => {}
}
}
let bytes = read_drawing_snapshot(path).map_err(|error| {
ReaderFailure::terminal(format!(
"failed to open drawing (is it in use by another program?): {error}"
))
})?;
let mut reader = DwgReader::from_stream_with_options(std::io::Cursor::new(bytes), options);
if let Some(progress) = progress {
reader.set_progress_callback(progress);
}
reader
};
.read_with_stats()
.map_err(ReaderFailure::from_reader)
}
#[cfg(target_arch = "wasm32")]
let mut reader = DwgReader::from_file_with_options(path, options)
.map_err(ReaderFailure::from_reader)?;
if let Some(progress) = progress {
reader.set_progress_callback(progress);
{
let mut reader = DwgReader::from_file_with_options(path, options)
.map_err(ReaderFailure::from_reader)?;
if let Some(progress) = progress {
reader.set_progress_callback(progress);
}
reader
.read_with_stats()
.map_err(ReaderFailure::from_reader)
}
}
/// Windows cloud-placeholder files (OneDrive "Files On Demand" and similar)
/// page their bytes in on demand; faulting a placeholder through a memory map
/// can raise an in-page exception that kills the process. Plain reads hydrate
/// the file instead. Checks `FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS`,
/// `FILE_ATTRIBUTE_RECALL_ON_OPEN` and `FILE_ATTRIBUTE_OFFLINE`.
#[cfg(all(not(target_arch = "wasm32"), target_os = "windows"))]
fn cloud_placeholder(path: &Path) -> bool {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_OFFLINE: u32 = 0x1000;
const FILE_ATTRIBUTE_RECALL_ON_OPEN: u32 = 0x0004_0000;
const FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS: u32 = 0x0040_0000;
std::fs::metadata(path)
.map(|meta| {
let attrs = meta.file_attributes();
attrs
& (FILE_ATTRIBUTE_OFFLINE
| FILE_ATTRIBUTE_RECALL_ON_OPEN
| FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS)
!= 0
})
.unwrap_or(false)
}
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
fn cloud_placeholder(_path: &Path) -> bool {
false
}
/// Read the whole drawing through the most permissive share mode the platform
/// offers. On Windows `std::fs::read` opens without `FILE_SHARE_DELETE`, so a
/// concurrent atomic-save replace makes the read fail even though every byte
/// was readable. A snapshot can never fault: a writer shrinking the file
/// mid-read just ends the read early.
#[cfg(not(target_arch = "wasm32"))]
fn read_drawing_snapshot(path: &Path) -> std::io::Result<Vec<u8>> {
#[cfg(target_os = "windows")]
{
use std::io::Read;
use std::os::windows::fs::OpenOptionsExt;
use windows_sys::Win32::Storage::FileSystem::{
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
};
let mut file = std::fs::OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
.open(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
#[cfg(not(target_os = "windows"))]
{
std::fs::read(path)
}
reader
.read_with_stats()
.map_err(ReaderFailure::from_reader)
}
fn read_dxf_path(path: &Path, failsafe: bool) -> Result<acadrust::ReadOutcome, ReaderFailure> {