Compare commits

...
Author SHA1 Message Date
Dylan M. Taylor 1d13c2ed55 address review: drop the duplicate status block; the combined docs update records this change 2026-11-11 20:31:46 +00:00
Dylan M. Taylor ebb6a3e747 fix(formats): copy an XLSX shared string only for a cell that is kept 2026-11-10 21:38:34 -05:00
Dylan M. Taylor 92e0e67f3a Clipboard: size-check a Wayland clipboard picture before anything decodes it (#5441)
* fix(clipboard): size-check a Wayland clipboard picture before it is decoded

* address review: refuse a clipboard PNG the renderer cannot decode within its budget
2026-11-10 21:33:46 -05:00
Dylan M. Taylor a34c94e4c1 packaging: use [[ for bash tests (SonarCloud S7688) (#6224)
Behaviour-preserving: `[[ ]]` in place of `[ ]` for bash tests (SonarCloud shell:S7688). Every script has a bash shebang; no test uses `-a`/`-o`, escaped parentheses or an unquoted pattern on the right of `=`. `bash -n` passes on each script; the rest is left to CI.

- `packaging/freebsd/package.sh`
2026-11-10 18:45:02 -05:00
FredApps a42c9eaab3 Merge pull request #6187 from storytold/sonar/format-mandated-sha1
sonar: SHA-1 in ODT decryption is the format's, not a choice
2026-11-10 18:43:18 +01:00
5 changed files with 61 additions and 12 deletions
+2
View File
@@ -272,6 +272,8 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
- 2026-11-03: Security: pictures are measured, and checked for transparency for tight text wrapping, from a PNG's chunk headers, instead of through the decoder, whose header read inflates an embedded ICC profile of up to 512 MiB for every picture it only measures. Readiness unchanged.
- 2026-11-03: Security: saving an embedded object (a native document's kept markup included) collects the relationship ids its markup uses in a set and checks them against the writable ones in one pass, instead of scanning every id collected so far for each attribute. Readiness unchanged.
- 2026-11-03: Security: the renderer leaves out a drawing path reaching more than a million device pixels (64 times the largest canvas) or of no finite size, which the rasteriser would otherwise flatten into points by the billion before clipping it. Readiness unchanged.
- 2026-11-03: Security: a picture pasted from the Wayland clipboard is read encoded under the 200 MB paste cap and refused from its header when it is too large, before anything decodes it; arboard decoded and converted it to RGBA with no limits first (as on Windows, #3623). Readiness unchanged.
- 2026-11-03: Security: XLSX import copies a cell's shared string only once the cell is kept (within the text budget, columns and rows), instead of copying it for every referring cell before checking. Readiness unchanged.
- 2026-11-03: Language packs: language-packs-2 is published and WordCraft pins its index: 75 languages, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch (#4145, #2505) and Swedish (Sweden, Finland), which need craft-proof's flag-based compounding (storytold/craft-libs#497, pinned). English (Australia) is checked against SCOWL's en-AU list as well as en-GB's (storytold/craft-libs#496). Mongolian and Thai stay hidden. Readiness unchanged.
- 2026-11-03: Language lists (CP4.3): a language installed from a Language pack (Bulgarian, say) can now be chosen. Set Proofing Language lists every built-in language and every installed or available pack's, each once and sorted by name; Review › Language and the status bar's menu add the installed packs' languages to the short list, with one way of working out the check mark, by tag. A pack's language is stored as a tag that finds the pack again (`sr-Latn-RS` is proofed by the Latin `sr-latn` pack, not the Cyrillic one; Valencian is Word's `ca-ES-valencia`). Every pack in language-packs-2 now has an English name (Belarusian used to show its own name, Беларуская). Readiness unchanged.
- 2026-11-03: Language packs (CP4.3): on a fresh install (or after an upgrade, with the list cached for an older pin) the missing-pack bar still offers a document language that has no dictionary, "Turkish proofing isn't installed. Install": the person's click turns on downloading, fetches the list and installs the pack if it lists one (if not, the bar goes). With downloading already on, a missing or stale list is fetched once in the background. Choosing a language with only the caret counts before anything is typed, and the spelling context menu's heading (Possible spelling mistake) is in the interface language, not the text's. Closing the bar is remembered per document, per pack and, before the list is known, per language tag. Readiness unchanged.
+23 -2
View File
@@ -108,8 +108,8 @@ pub fn formats() -> Vec<String> {
/// The picture on the clipboard: copied files, else image pixels, else (Wayland) an encoded
/// picture in a format arboard doesn't decode. A picture that is there but can't be read says
/// why (#833). The UI checks the sizes before inserting anything; on Windows they are checked
/// from the header before anything is decoded (#3623).
/// why (#833). The UI checks the sizes before inserting anything; on Windows and Wayland they are
/// checked from the header before anything is decoded (#3623).
pub fn picture() -> Option<ClipboardPicture> {
let mut clipboard = match arboard::Clipboard::new() {
Ok(c) => c,
@@ -136,6 +136,10 @@ fn decoded_picture(_: &mut arboard::Clipboard) -> Option<ClipboardPicture> {
/// The picture arboard decodes from the clipboard, else an encoded one read past it.
#[cfg(not(target_os = "windows"))]
fn decoded_picture(clipboard: &mut arboard::Clipboard) -> Option<ClipboardPicture> {
#[cfg(all(unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten"))))]
if let Some(picture) = platform::picture() {
return Some(picture);
}
match clipboard.get_image() {
Ok(image) => Some(ClipboardPicture::Pixels { width: image.width, height: image.height, rgba: image.bytes.into_owned() }),
Err(arboard::Error::ContentNotAvailable) => platform::encoded_picture().map(ClipboardPicture::Encoded),
@@ -160,6 +164,7 @@ mod platform {
use std::io::Read;
use wl_clipboard_rs::paste::{ClipboardType, MimeType, Seat, get_contents, get_mime_types_ordered};
use wordcraft_ui_egui::paste_picture::{self, ClipboardPicture};
fn wayland() -> bool {
std::env::var_os("WAYLAND_DISPLAY").is_some()
@@ -364,6 +369,22 @@ mod platform {
assert!(complete(0, 0, 0));
}
}
/// The Wayland clipboard's picture, read encoded under the paste's byte cap and refused from
/// its header when a paste can't take its size, before anything decodes it. arboard decodes
/// and converts the PNG to RGBA with no size limits. `None` when Wayland offers no picture
/// (or isn't reachable): arboard is asked then, which also covers X11.
pub fn picture() -> Option<ClipboardPicture> {
if !wayland() {
return None;
}
let offered = formats();
let mime = super::PICTURE_MIMES.into_iter().find(|m| offered.iter().any(|o| o == m))?;
Some(match read(mime, wordcraft_ui_egui::paste_picture::MAX_FILE_BYTES) {
Some(bytes) => wordcraft_ui_egui::paste_picture::encoded_picture(bytes),
None => ClipboardPicture::Unreadable("the clipboard picture is larger than 200 MB, or couldn't be read".into()),
})
}
}
#[cfg(target_os = "windows")]
+11 -6
View File
@@ -1,3 +1,5 @@
use std::borrow::Cow;
use super::date::{DateKind, builtin_date, custom_date, iso_datetime, serial_date};
use super::{
BTreeMap, Ev, Grid, HashMap, MAX_CELLS, MAX_COLS, MAX_ROWS, MAX_SHARED, MAX_SHEETS, MAX_XFS, Package, TextBudget, attr, push_capped, walk,
@@ -263,7 +265,7 @@ pub(super) fn xlsx_rows(pkg: &mut Package<'_>, path: &str) -> Result<Vec<Vec<Str
row.resize(c.col + 1, String::new());
}
if let Some(slot) = row.get_mut(c.col) {
*slot = text;
*slot = text.into_owned();
}
}
_ => {}
@@ -276,12 +278,15 @@ pub(super) fn xlsx_rows(pkg: &mut Package<'_>, path: &str) -> Result<Vec<Vec<Str
Ok(grid.rows)
}
fn xlsx_value(c: &XCell, shared: &[String], formats: &[Option<DateKind>], date1904: bool) -> String {
/// A cell's text: a shared string borrowed, so a cell the sheet then refuses (past the text
/// budget, the columns or the rows) costs no copy of it.
fn xlsx_value<'a>(c: &XCell, shared: &'a [String], formats: &[Option<DateKind>], date1904: bool) -> Cow<'a, str> {
let v = c.value.trim();
let kind = c.kind.trim();
if kind.eq_ignore_ascii_case("s") {
v.parse::<usize>().ok().and_then(|i| shared.get(i)).cloned().unwrap_or_default()
} else if kind.eq_ignore_ascii_case("inlineStr") {
return Cow::Borrowed(v.parse::<usize>().ok().and_then(|i| shared.get(i)).map_or("", String::as_str));
}
Cow::Owned(if kind.eq_ignore_ascii_case("inlineStr") {
if c.inline.is_empty() { c.value.clone() } else { c.inline.clone() }
} else if kind.eq_ignore_ascii_case("str") || kind.eq_ignore_ascii_case("e") {
c.value.clone()
@@ -292,12 +297,12 @@ fn xlsx_value(c: &XCell, shared: &[String], formats: &[Option<DateKind>], date19
} else if v.is_empty() {
c.inline.clone()
} else {
let Ok(x) = v.parse::<f64>() else { return v.to_string() };
let Ok(x) = v.parse::<f64>() else { return Cow::Owned(v.to_string()) };
match formats.get(c.style).copied().flatten() {
Some(kind) => serial_date(x, date1904, kind).unwrap_or_else(|| number(x)),
None => number(x),
}
}
})
}
/// A number as a spreadsheet shows it in General format: up to 15 significant digits, no
+22 -1
View File
@@ -67,11 +67,16 @@ fn too_large_to_paste(width: u32, height: u32) -> ClipboardPicture {
ClipboardPicture::Unreadable(format!("the clipboard picture is too large to paste ({width} × {height} pixels)"))
}
/// Whether the renderer can decode `bytes` within its budget (a 16-bit picture takes more than its pixel count says).
fn probe_fits(bytes: &[u8]) -> bool {
wordcraft_geom::picture::probe(bytes).is_ok_and(|p| p.fits(wordcraft_geom::MAX_DECODED_BYTES))
}
/// An encoded picture the host read from the clipboard (the Windows "PNG" format), refused from
/// its header when a paste can't take its size, before anything decodes it (#3623).
pub fn encoded_picture(bytes: Vec<u8>) -> ClipboardPicture {
match wordcraft_geom::picture::probe(&bytes).map(|p| (p.width, p.height)) {
Ok((w, h)) if pastes(w, h) => ClipboardPicture::Encoded(bytes),
Ok((w, h)) if pastes(w, h) && probe_fits(&bytes) => ClipboardPicture::Encoded(bytes),
Ok((w, h)) => too_large_to_paste(w, h),
Err(e) => ClipboardPicture::Unreadable(e.to_string()),
}
@@ -258,6 +263,22 @@ mod tests {
image::GrayImage::new(20_000, 1).write_to(&mut std::io::Cursor::new(&mut wide), image::ImageFormat::Png).unwrap();
assert!(matches!(encoded_picture(wide), ClipboardPicture::Unreadable(e) if e.contains("too large")));
assert!(matches!(encoded_picture(b"not a picture".to_vec()), ClipboardPicture::Unreadable(_)));
// 8000 × 6000 passes the pixel limits, but 16-bit RGBA needs 12 bytes a pixel to decode.
let mut big = Vec::new();
image::DynamicImage::ImageRgba16(image::ImageBuffer::new(1, 1))
.write_to(&mut std::io::Cursor::new(&mut big), image::ImageFormat::Png)
.unwrap();
big[16..20].copy_from_slice(&8000u32.to_be_bytes());
big[20..24].copy_from_slice(&6000u32.to_be_bytes());
let mut crc = 0xFFFF_FFFFu32;
for &b in &big[12..29] {
crc ^= u32::from(b);
for _ in 0..8 {
crc = if crc & 1 == 1 { 0xEDB8_8320 ^ (crc >> 1) } else { crc >> 1 };
}
}
big[29..33].copy_from_slice(&(!crc).to_be_bytes());
assert!(matches!(encoded_picture(big), ClipboardPicture::Unreadable(e) if e.contains("too large")));
// BGRA in memory: a half-transparent red pixel and an opaque blue one.
let ClipboardPicture::Pixels { width, height, rgba } = dib_picture(dib(2, 1, &[[0, 0, 255, 128], [255, 0, 0, 255]])) else {
panic!("pixels")
+3 -3
View File
@@ -55,7 +55,7 @@ esac
# Only binaries for this machine run here (the --version check at the end); a FreeBSD target built
# on another OS never does.
HOST_ARCH="$(release_arch "$(uname -m)" 2>/dev/null)" || HOST_ARCH="$(uname -m)"
if [ -n "${CARGO_BUILD_TARGET:-}" ] && [ "$(uname -s)" != FreeBSD ]; then
if [[ -n "${CARGO_BUILD_TARGET:-}" ]] && [[ "$(uname -s)" != FreeBSD ]]; then
HOST_ARCH="$(uname -s) $HOST_ARCH"
fi
BASENAME="wordcraft-$VERSION-freebsd-$ARCH"
@@ -96,12 +96,12 @@ mkdir -p "$DIST"
tar -C "$WORK" -czf "$DIST/$BASENAME.tar.gz" "$BASENAME"
echo "wrote $DIST/$BASENAME.tar.gz"
if [ -z "${CARGO_BUILD_TARGET:-}" ] && [ "$ARCH" = "$HOST_ARCH" ]; then
if [[ -z "${CARGO_BUILD_TARGET:-}" ]] && [[ "$ARCH" = "$HOST_ARCH" ]]; then
"$STAGE/bin/wordcraft-cli" --version
else
# They cannot run here, so check what they are instead: executables, not libraries or objects.
# An older file(1) calls a PIE a shared object; its interpreter still marks it as a program.
want="$ARCH"; [ "$ARCH" != x86_64 ] || want=x86-64
want="$ARCH"; [[ "$ARCH" != x86_64 ]] || want=x86-64
for b in "$STAGE"/bin/*; do
case "$(file -b "$b")" in
ELF*" executable, "*"$want"*FreeBSD* | ELF*"$want"*FreeBSD*interpreter*) ;;