//! Pre-2007 DXF text is in the code page named by `$DWGCODEPAGE` (#505): Cyrillic, Central //! European, Greek, Japanese, Chinese and Korean drawings open with their layer names, block names //! and text intact, in ASCII and binary DXF, and a hostile file never panics. use cadcraft_doc::{Drawing, EntityKind}; /// An ASCII DXF of `version` declaring `code_page`, with a layer, a block and a TEXT whose names /// and value are the raw bytes given. fn ascii_dxf(version: &str, code_page: &str, layer: &[u8], block: &[u8], text: &[u8]) -> Vec { let mut b = Vec::new(); let mut put = |code: i32, v: &[u8]| { b.extend_from_slice(format!("{code:>3}\r\n").as_bytes()); b.extend_from_slice(v); b.extend_from_slice(b"\r\n"); }; for (c, v) in [(0, &b"SECTION"[..]), (2, b"HEADER"), (9, b"$ACADVER"), (1, version.as_bytes()), (9, b"$DWGCODEPAGE"), (3, code_page.as_bytes())] { put(c, v); } for (c, v) in [(0, &b"ENDSEC"[..]), (0, b"SECTION"), (2, b"TABLES"), (0, b"TABLE"), (2, b"LAYER"), (0, b"LAYER"), (2, layer), (70, b"0")] { put(c, v); } for (c, v) in [(62, &b"7"[..]), (6, b"CONTINUOUS"), (0, b"ENDTAB"), (0, b"ENDSEC"), (0, b"SECTION"), (2, b"BLOCKS"), (0, b"BLOCK")] { put(c, v); } for (c, v) in [(8, &b"0"[..]), (2, block), (70, b"0"), (10, b"0"), (20, b"0"), (30, b"0"), (0, b"LINE"), (8, b"0")] { put(c, v); } for (c, v) in [(10, &b"0"[..]), (20, b"0"), (11, b"1"), (21, b"1"), (0, b"ENDBLK"), (0, b"ENDSEC"), (0, b"SECTION"), (2, b"ENTITIES")] { put(c, v); } for (c, v) in [(0, &b"TEXT"[..]), (8, layer), (10, b"0"), (20, b"0"), (40, b"1"), (1, text), (0, b"INSERT"), (8, b"0")] { put(c, v); } for (c, v) in [(2, block), (10, b"5"), (20, b"5"), (0, b"ENDSEC"), (0, b"EOF")] { put(c, v); } b } fn text_of(d: &Drawing) -> String { d.model.iter().find_map(|e| if let EntityKind::Text(t) = &e.kind { Some(t.value.clone()) } else { None }).unwrap_or_default() } fn names(d: &Drawing) -> (String, String, String) { // The TEXT's layer, which the LAYER table defines. let layer = d.model.iter().find(|e| matches!(e.kind, EntityKind::Text(_))).map(|e| e.common.layer.clone()).unwrap_or_default(); assert!(d.layers.iter().any(|l| l.name == layer), "layer `{layer}` is defined"); let insert = d.model.iter().find_map(|e| if let EntityKind::Insert(i) = &e.kind { Some(i.block.clone()) } else { None }); let block = insert.unwrap_or_default(); assert!(d.block(&block).is_some(), "block `{block}` is defined"); (layer, block, text_of(d)) } /// Text, layer and block names decode from each code page AutoCAD writes pre-2007 files in. #[test] fn reads_text_in_its_code_page() { // (code page, layer bytes, layer, text bytes, text); byte sequences from the code page tables. let cases: [(&str, &[u8], &str, &[u8], &str); 10] = [ ("ANSI_1251", b"\xD1\xEB\xEE\xE9", "Слой", b"\xCF\xF0\xE8\xE2\xE5\xF2", "Привет"), ("ANSI_1250", b"\xA3\xF3d\x9F", "Łódź", b"\x8Ail\xE1", "Šilá"), ("ANSI_1253", b"\xD3\xF7\xDD\xE4\xE9\xEF", "Σχέδιο", b"\xE1\xE2\xE3", "αβγ"), ("ANSI_1254", b"\xC7izim", "Çizim", b"\xDE\xF0", "Şğ"), ("ANSI_874", b"\xC0\xD2\xBE", "ภาพ", b"\xC0\xD2\xBE", "ภาพ"), ("ANSI_932", b"\x90\x7D\x96\xCA", "図面", b"\x90\x7D\x96\xCA", "図面"), ("ANSI_936", b"\xCD\xBC\xB2\xE3", "图层", b"\xCD\xBC\xB2\xE3", "图层"), ("ANSI_949", b"\xB5\xB5\xB8\xE9", "도면", b"\xB5\xB5\xB8\xE9", "도면"), ("ANSI_950", b"\xB9\xCF\xBC\x68", "圖層", b"\xB9\xCF\xBC\x68", "圖層"), ("DOS850", b"Gr\x94\xE1e", "Größe", b"\x9B\x9D", "øØ"), ]; for (cp, layer_bytes, layer, text_bytes, text) in cases { let mut block_bytes = b"B_".to_vec(); block_bytes.extend_from_slice(layer_bytes); let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", cp, layer_bytes, &block_bytes, text_bytes)).unwrap(); assert_eq!(names(&d), (layer.to_string(), format!("B_{layer}"), text.to_string()), "{cp}"); } } /// A Shift-JIS trail byte can be 0x5C (`\`): 表 is 95 5C. Decoding the bytes before the escapes /// keeps it a kanji instead of starting a `\U+` escape; real escapes still decode. #[test] fn shift_jis_backslash_trail_byte_is_not_an_escape() { let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", "ANSI_932", b"L", b"B", b"\x95\x5CU+0041 \\U+00E9 \\M+5BCFE")).unwrap(); assert_eq!(text_of(&d), "表U+0041 é 件"); } /// R2007+ files and files whose bytes are valid UTF-8 are UTF-8, whatever code page they declare /// (the DWG bridge writes UTF-8 with the DWG's code page name); invalid UTF-8 in a 2007+ file /// falls back to the declared code page. #[test] fn utf8_wins_when_valid() { let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", "ANSI_1251", "Слой".as_bytes(), b"B", "Привет".as_bytes())).unwrap(); assert_eq!(names(&d), ("Слой".into(), "B".into(), "Привет".into())); let d = cadcraft_io::read_dxf(&ascii_dxf("AC1032", "ANSI_1252", "图层".as_bytes(), b"B", "Café".as_bytes())).unwrap(); assert_eq!(names(&d), ("图层".into(), "B".into(), "Café".into())); let d = cadcraft_io::read_dxf(&ascii_dxf("AC1021", "ANSI_1251", b"\xD1\xEB\xEE\xE9", b"B", b"x")).unwrap(); assert_eq!(names(&d).0, "Слой"); } /// A code page CADCraft doesn't know (or none) reads as ANSI_1252, as before. #[test] fn unknown_code_page_falls_back_to_ansi_1252() { for cp in ["ANSI_1361", "EBCDIC", "", "\u{1}"] { let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", cp, b"Caf\xE9", b"B", b"\x80 \xD8")).unwrap(); assert_eq!(names(&d), ("Café".into(), "B".into(), "€ Ø".into()), "{cp:?}"); } } /// Reading converts to Unicode; saving writes pure ASCII with `\U+` escapes, which reads back the /// same. #[test] fn code_page_text_survives_a_round_trip() { let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", "ANSI_1251", b"\xD1\xEB\xEE\xE9", b"\xC1\xEB\xEE\xEA", b"\xCF\xF0\xE8\xE2\xE5\xF2")).unwrap(); let out = cadcraft_io::write_dxf(&d); assert!(out.is_ascii()); let back = cadcraft_io::read_dxf(out.as_bytes()).unwrap(); assert_eq!(names(&back), ("Слой".into(), "Блок".into(), "Привет".into())); } /// A binary DXF: R2000 strings are in the code page, R2007+ strings UTF-8. #[test] fn binary_dxf_honours_the_code_page() { let bin = |version: &str, cp: &str, text: &[u8]| { let mut b = b"AutoCAD Binary DXF\r\n\x1a\0".to_vec(); let mut s = |code: i16, v: &[u8]| { b.extend_from_slice(&code.to_le_bytes()); b.extend_from_slice(v); b.push(0); }; s(0, b"SECTION"); s(2, b"HEADER"); s(9, b"$ACADVER"); s(1, version.as_bytes()); s(9, b"$DWGCODEPAGE"); s(3, cp.as_bytes()); s(0, b"ENDSEC"); s(0, b"SECTION"); s(2, b"ENTITIES"); s(0, b"TEXT"); s(8, b"0"); b.extend_from_slice(&10i16.to_le_bytes()); b.extend_from_slice(&0f64.to_le_bytes()); b.extend_from_slice(&20i16.to_le_bytes()); b.extend_from_slice(&0f64.to_le_bytes()); b.extend_from_slice(&40i16.to_le_bytes()); b.extend_from_slice(&1f64.to_le_bytes()); let mut s = |code: i16, v: &[u8]| { b.extend_from_slice(&code.to_le_bytes()); b.extend_from_slice(v); b.push(0); }; s(1, text); s(0, b"ENDSEC"); s(0, b"EOF"); b }; let read = |b: &[u8]| text_of(&cadcraft_io::read_dxf(b).unwrap()); assert_eq!(read(&bin("AC1015", "ANSI_1251", b"\xCF\xF0\xE8\xE2\xE5\xF2")), "Привет"); assert_eq!(read(&bin("AC1018", "ANSI_932", b"\x95\x5C\\U+00E9")), "表é"); // A short Cyrillic string that is also valid UTF-8 (D1 B2) is still read in the code page. assert_eq!(read(&bin("AC1015", "ANSI_1251", b"\xD1\xB2")), "СІ"); assert_eq!(read(&bin("AC1021", "ANSI_1251", "Привет".as_bytes())), "Привет"); assert_eq!(read(&bin("AC1015", "ANSI_1361", b"Caf\xE9")), "Café"); } /// Truncated and invalid multibyte sequences become U+FFFD; nothing panics and the structure of /// the file (line ends, group codes) survives. #[test] fn hostile_multibyte_never_panics() { // A lead byte cut off by the line end, a lone trail byte, an invalid pair. let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", "ANSI_932", b"\x90", b"\x81", b"A\x90")).unwrap(); assert_eq!(names(&d), ("\u{FFFD}".into(), "\u{FFFD}".into(), "A\u{FFFD}".into())); let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", "ANSI_936", b"\xFF\xFF", b"B", b"\xA1")).unwrap(); assert_eq!(text_of(&d), "\u{FFFD}"); // A file cut off inside a double-byte character, garbage after the header, every byte value. let mut cut = ascii_dxf("AC1015", "ANSI_949", b"L", b"B", b"x"); cut.truncate(cut.len() - 10); cut.push(0xB5); let _ = cadcraft_io::read_dxf(&cut); let all: Vec = (0..=255u8).collect(); for cp in ["ANSI_932", "ANSI_936", "ANSI_949", "ANSI_950", "ANSI_1251", "DOS437", "DOS852", "MACINTOSH"] { let d = cadcraft_io::read_dxf(&ascii_dxf( "AC1015", cp, b"L", b"B", &all.iter().copied().filter(|&b| b != b'\n' && b != b'\r').collect::>(), )); assert!(d.is_ok(), "{cp}"); let mut junk = ascii_dxf("AC1015", cp, b"L", b"B", b"x"); junk.extend_from_slice(&all); let _ = cadcraft_io::read_dxf(&junk); } } /// A pre-2007 DWG keeps its code page through the acadrust bridge: a Cyrillic R2000 DXF saved as /// DWG (acadrust writes 2004, text in ANSI_1251) opens with its text. #[cfg(not(target_arch = "wasm32"))] #[test] fn pre_2007_dwg_text_decodes_through_the_bridge() { let dxf = ascii_dxf("AC1015", "ANSI_1251", b"\xD1\xEB\xEE\xE9", b"\xC1\xEB\xEE\xEA", b"\xCF\xF0\xE8\xE2\xE5\xF2"); let dwg = cadcraft_dwg::dxf_to_dwg(&dxf).unwrap(); let d = cadcraft_io::read(&dwg, "x.dwg").unwrap(); assert_eq!(names(&d), ("Слой".into(), "Блок".into(), "Привет".into())); }