mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 20:37:10 -06:00
Merge PR #3269: #3003: Save to a NAS share mounted by GVFS keeps working when it refuses a mode change
ROADMAP.md,docs/file-format-parity.md: status lines keep main's with the PR's item added; tables and lists union.
This commit is contained in:
+3
-1
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ pub(super) fn write(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||
let written = replace_with(path, private, |file| {
|
||||
file.write_all(bytes)?;
|
||||
if let Some(permissions) = permissions {
|
||||
file.set_permissions(permissions)?;
|
||||
keep_permissions(file.set_permissions(permissions))?;
|
||||
}
|
||||
// Synchronize the prepared file before replacing the destination. Directory durability
|
||||
// and network-filesystem/power-loss guarantees are outside this operation's contract.
|
||||
@@ -33,6 +33,34 @@ pub(super) fn write(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||
written.map_err(explain)
|
||||
}
|
||||
|
||||
/// A folder that keeps no Unix modes refuses to set one with "Operation not supported": a NAS
|
||||
/// share mounted by GVFS (`smb://` in the Linux file manager) refuses every mode change (#3003).
|
||||
/// There is no mode to keep there, so the save goes on; any other failure still stops it.
|
||||
fn keep_permissions(set: io::Result<()>) -> io::Result<()> {
|
||||
match set {
|
||||
Err(e) if not_supported(&e) => Ok(()),
|
||||
set => set,
|
||||
}
|
||||
}
|
||||
|
||||
/// ENOTSUP and EOPNOTSUPP, which std doesn't count as `Unsupported` (only ENOSYS), plus that kind.
|
||||
fn not_supported(e: &io::Error) -> bool {
|
||||
const CODES: &[i32] = if cfg!(any(target_os = "linux", target_os = "android")) {
|
||||
&[95]
|
||||
} else if cfg!(any(target_os = "macos", target_os = "ios")) {
|
||||
&[45, 102]
|
||||
} else if cfg!(target_os = "openbsd") {
|
||||
&[45, 91]
|
||||
} else if cfg!(target_os = "netbsd") {
|
||||
&[45, 86]
|
||||
} else if cfg!(any(target_os = "freebsd", target_os = "dragonfly")) {
|
||||
&[45]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
e.kind() == io::ErrorKind::Unsupported || e.raw_os_error().is_some_and(|code| CODES.contains(&code))
|
||||
}
|
||||
|
||||
fn replace_with(path: &Path, private: bool, prepare: impl FnOnce(&mut File) -> io::Result<()>) -> io::Result<()> {
|
||||
let mut temp = match Temporary::create(path, private) {
|
||||
Ok(temp) => temp,
|
||||
@@ -245,6 +273,43 @@ mod tests {
|
||||
assert_eq!(fs::read_dir(&d.0).unwrap().count(), 2);
|
||||
}
|
||||
|
||||
/// #3003: a folder that refuses mode changes (a GVFS-mounted NAS share) still takes the
|
||||
/// save; a refused mode change for any other reason still stops it and keeps the original.
|
||||
#[test]
|
||||
fn a_folder_without_unix_modes_still_takes_the_save() {
|
||||
let d = Scratch::new();
|
||||
let path = d.target();
|
||||
fs::write(&path, b"Original bytes.").unwrap();
|
||||
replace_with(&path, true, |file| {
|
||||
file.write_all(b"Saved on the share.")?;
|
||||
keep_permissions(Err(io::Error::from(io::ErrorKind::Unsupported)))?;
|
||||
file.sync_all()
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(fs::read(&path).unwrap(), b"Saved on the share.");
|
||||
assert_eq!(fs::read_dir(&d.0).unwrap().count(), 1);
|
||||
|
||||
let err = replace_with(&path, true, |file| {
|
||||
file.write_all(b"Refused.")?;
|
||||
keep_permissions(Err(io::Error::from(io::ErrorKind::PermissionDenied)))
|
||||
})
|
||||
.unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
|
||||
assert_eq!(fs::read(&path).unwrap(), b"Saved on the share.");
|
||||
assert_eq!(fs::read_dir(&d.0).unwrap().count(), 1);
|
||||
}
|
||||
|
||||
/// GVFS reports a refused mode change as ENOTSUP, which std leaves uncategorized; other OS
|
||||
/// errors still stop the save.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
#[test]
|
||||
fn enotsup_from_the_os_is_an_unsupported_mode_change() {
|
||||
let enotsup = if cfg!(target_os = "linux") { 95 } else { 45 };
|
||||
assert!(keep_permissions(Err(io::Error::from_raw_os_error(enotsup))).is_ok());
|
||||
let eacces = 13;
|
||||
assert!(keep_permissions(Err(io::Error::from_raw_os_error(eacces))).is_err());
|
||||
}
|
||||
|
||||
/// Makes `dir` read-only while `f` runs. `None` when running as root, where the folder isn't
|
||||
/// read-only to us.
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** trivial (shape gradients and transparency, #1500; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `<blockquote>` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** trivial (shape gradients and transparency, #1500; previously trivial: a folder that keeps no Unix modes, such as a NAS share mounted by GVFS, still takes a save, #3003; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `<blockquote>` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
|
||||
|
||||
Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the
|
||||
share of a typical real-world file's content and formatting that survives (read: shows correctly;
|
||||
@@ -159,7 +159,7 @@ The bibliography names used by #532 are reserved; bibliography already loaded in
|
||||
model belongs to its writer. Word-derived files are not fixtures. No readiness estimate rises
|
||||
from these synthetic package checks, and native Word rendering/binding behavior remains untested.
|
||||
## Native save-path guarantees
|
||||
Native `io::save_path_with` serializes (and, for password-protected DOCX, encrypts) before creating an exclusively owned sibling temporary file. It writes and synchronizes that file, closes it, then replaces the destination. Failed preparation or replacement keeps the original bytes; cleanup removes only the save's temporary file, on a best-effort basis. Existing `<name>.tmp` files, directories and links are untouched. A new document gets the default mode (`0666` less the umask). When replacing an existing regular file, the Unix temporary file starts with mode `0600` and then takes the existing file's basic permissions. Serialization and format support are unchanged. Native `io::write_file` (Print to PDF, `file.printPdf`; Export Page as PNG, `file.exportPng`) writes through the same path (#1355). If the folder refuses the temporary file with a permission error (macOS privacy protection, a read-only folder; #2138), an existing destination is never written in place: it stays byte for byte as it was, and the error says to use Save As to another folder or to give WordCraft access to the folder (on macOS, System Settings › Privacy & Security › Files and Folders). Only a destination that doesn't exist yet is created directly (exclusively, so nothing already there is opened); a failed direct write removes it again. Unit tests cover a read-only folder on Unix; the macOS privacy case is not yet confirmed on a Mac.
|
||||
Native `io::save_path_with` serializes (and, for password-protected DOCX, encrypts) before creating an exclusively owned sibling temporary file. It writes and synchronizes that file, closes it, then replaces the destination. Failed preparation or replacement keeps the original bytes; cleanup removes only the save's temporary file, on a best-effort basis. Existing `<name>.tmp` files, directories and links are untouched. A new document gets the default mode (`0666` less the umask). When replacing an existing regular file, the Unix temporary file starts with mode `0600` and then takes the existing file's basic permissions. Serialization and format support are unchanged. Native `io::write_file` (Print to PDF, `file.printPdf`; Export Page as PNG, `file.exportPng`) writes through the same path (#1355). If the folder refuses the temporary file with a permission error (macOS privacy protection, a read-only folder; #2138), an existing destination is never written in place: it stays byte for byte as it was, and the error says to use Save As to another folder or to give WordCraft access to the folder (on macOS, System Settings › Privacy & Security › Files and Folders). Only a destination that doesn't exist yet is created directly (exclusively, so nothing already there is opened); a failed direct write removes it again. Unit tests cover a read-only folder on Unix; the macOS privacy case is not yet confirmed on a Mac. A folder that keeps no Unix modes refuses to set one with "Operation not supported" (a NAS share mounted by GVFS, `smb://` in the Linux file manager, refuses every mode change; #3003): the save goes on without copying the mode, since there is none to keep; any other failure to set it still stops the save and keeps the original. Unit tests cover the refusal; not yet confirmed on a real GVFS share.
|
||||
Synthetic CLI cases and `engine/tests/safe_save.rs` cover occupied names, links, blocked replacement, missing directories, Save As session state, password-protected saves and original-file protection on macOS. Unit tests inject preparation errors and interleave two prepared saves. Independent saves use separate files; the last successful replacement wins. This is not document locking or conflict detection.
|
||||
Windows/Linux runtime behavior, ACLs and extended metadata, real disk-full conditions, power loss and network filesystems are not verified by these tests. The containing directory is not explicitly synchronized. Cleanup can fail when storage or permissions change, and process termination can leave a temporary file. No Word observation was needed for this filesystem change; it makes no new format-fidelity claim.
|
||||
## What decides beta for formats
|
||||
@@ -377,3 +377,4 @@ Imported text fields use the first nonempty cached result run's formatting on th
|
||||
| 2026-10-15 | trivial | Legacy form fields are drawn: check boxes as Word's squares (checked with an X), drop-downs with their selected entry |
|
||||
| 2026-10-17 | trivial | A folder that refuses a temporary file leaves an existing document untouched (the error says to use Save As or allow access to the folder); a new file is created directly (#2138) |
|
||||
| 2026-10-18 | trivial | Word 2007 WordArt (VML `v:textpath` in the body or a header logo) shown as a text box with its text, font, colour and warp, not dropped (#2875) |
|
||||
| 2026-10-20 | trivial | A folder that keeps no Unix modes (a NAS share mounted by GVFS) still takes a save instead of failing with "Operation not supported" (#3003) |
|
||||
|
||||
Reference in New Issue
Block a user