diff --git a/crates/doc-binary/src/tests/word6.rs b/crates/doc-binary/src/tests/word6.rs index 6bd9fb8f3..fa3f68a51 100644 --- a/crates/doc-binary/src/tests/word6.rs +++ b/crates/doc-binary/src/tests/word6.rs @@ -274,6 +274,19 @@ fn a_runs_font_charset_chooses_its_code_page() { assert_eq!(texts(&d), ["Da Да"]); } +/// Text in an East Asian code page (a Japanese font, charset 128: Shift-JIS) is refused, not +/// read a byte at a time into replacement characters (review of wordcraft#6345). +#[test] +fn east_asian_text_is_refused_not_lost() { + let fonts: &[(u8, &str)] = &[(0, "Times New Roman"), (128, "MS Mincho")]; + let runs: &[(u32, u32, &[u8])] = &[(0, 2, &[0x5D, 0x01, 0x00]), (2, 3, &[])]; + let d = Doc6 { text: b"\x93\xfa\r", fonts, runs, ..Default::default() }; + assert!(matches!(read(&word6_file(&d)), Err(DocbinError::NotWord(e)) if e.contains("East Asian"))); + // The same file in a Western font still opens. + let d = Doc6 { text: b"\x93\xfa\r", fonts, runs: &[], ..Default::default() }; + assert_eq!(texts(&d).len(), 1); +} + #[test] fn the_font_sprm_is_found_after_other_sprms() { // As in real files: bold-like toggles, a size, a picture anchor's length-prefixed sprm. @@ -302,15 +315,6 @@ fn an_unknown_sprm_leaves_font_0() { assert_eq!(texts(&d), ["Äà"]); } -#[test] -fn east_asian_font_bytes_read_as_replacement_characters() { - // A multibyte code page can't be read one byte per CP: ASCII stays, the rest is U+FFFD. - let text = b"ab\x82\xA0\r"; - let runs: &[(u32, u32, &[u8])] = &[(0, 4, &[0x5D, 0x01, 0x00]), (4, 5, &[])]; - let d = Doc6 { text, fonts: &[(0, "Arial"), (128, "MS Mincho")], runs, ..Default::default() }; - assert_eq!(texts(&d), ["ab\u{FFFD}\u{FFFD}"]); -} - #[test] fn the_toggle_sprms_before_the_font_are_framed() { // 0x57 (strike, a one-byte toggle in [MS-DOC]'s Prm0 isprm table) before the font sprm: the diff --git a/crates/doc-binary/src/word6.rs b/crates/doc-binary/src/word6.rs index 0139e3ce5..e9fab2d74 100644 --- a/crates/doc-binary/src/word6.rs +++ b/crates/doc-binary/src/word6.rs @@ -31,8 +31,9 @@ //! others were identified against Word. See [`chp`] and [`pap`]. //! - A font table entry is a size byte (the entry's length − 1), a flags byte, a 2-byte weight, the //! charset byte, one more byte, then the name in 8-bit characters. -//! - The stylesheet (pair 1) has the Word 97 shape with smaller parts: a 14-byte header whose -//! first two words are the style count and the size of a style's fixed part (8, Word 97: 10); +//! - The stylesheet (pair 1) has the Word 97 shape with smaller parts: a u16 header size (14), +//! then a header of that many bytes whose first two words are the style count and the size of +//! a style's fixed part (8, Word 97: 10); //! then per style a u16 size (0: an empty slot) and that many bytes: the fixed part (sti, then //! kind and base style, then UPX count and next style, then a size), the name (a length byte, //! 8-bit characters, a 0), and the UPXs, each a u16 size and its bytes, padded to even. A @@ -258,6 +259,13 @@ pub(crate) fn content(word: &[u8], fib: &Fib6) -> Result { _ => 0, }; let code_pages = if windows { code_page_runs(&chp_runs, &pap_runs, &facts, &fonts, default_code_page) } else { Vec::new() }; + // shortcut: East Asian (lead and trail byte) text is refused, as all Word 6.0/95 files were + // before; read it a character at a time when the piece walk can decode byte pairs. + let east_asian = || DocbinError::NotWord("Word 6.0/95 text in an East Asian code page is not read yet".into()); + if craft_codepage::is_multibyte_code_page(default_code_page) || code_pages.iter().any(|r| craft_codepage::is_multibyte_code_page(r.code_page)) { + return Err(east_asian()); + } + let multibyte_piece = std::cell::Cell::new(false); let text = Word6Text { wide: fib.wide, default_code_page, code_pages }; let pieces = if fib.complex { let (fc, lcb) = fib.pair(PAIR_CLX).ok_or_else(|| DocbinError::Malformed("the fast-saved file has no piece table".into()))?; @@ -267,12 +275,20 @@ pub(crate) fn content(word: &[u8], fib: &Fib6) -> Result { if !windows || prm & 1 == 0 { return None; } - last_font(prcs.get(usize::from(prm >> 1))?).and_then(|f| font_code_page(&fonts, f)) + let cp = last_font(prcs.get(usize::from(prm >> 1))?).and_then(|f| font_code_page(&fonts, f)); + if cp.is_some_and(craft_codepage::is_multibyte_code_page) { + multibyte_piece.set(true); + } + cp }; // The Prcs then carry Word 97 sprms, as the CHPXs and PAPXs do: the walk applies their // character sprms to the piece's text and their paragraph sprms to the paragraph whose mark // the piece holds. - PieceTable::parse_word6(word, fc, lcb, text, piece_code_page, translate_prc)? + let table = PieceTable::parse_word6(word, fc, lcb, text, piece_code_page, translate_prc)?; + if multibyte_piece.get() { + return Err(east_asian()); + } + table } else { // Word reads no more characters than the text's byte range (fcMin..fcMac) holds, one byte // each or two for UTF-16, whatever the character count says.