mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 04:55:39 +00:00
* craft-dxf: the ASCII reader unescapes only text that holds a backslash Only a string with a backslash can hold a \U+ or \M+ escape; every other string moves into the tag as it is instead of being copied. The EOF check reads the value before it moves. Ported from storytold/cadcraft#1833. * craft-dxf: write_ascii_in_page writes the code page without the UTF-8 fallback write_ascii_in falls back to pure ASCII when the code page bytes happen to be valid UTF-8, for readers that guess UTF-8. A reader that decodes a pre-2007 DXF strictly by its $DWGCODEPAGE needs the code page bytes. Ported from storytold/cadcraft#2851. * craft-dxf: behaviour-preserving cleanups Exact group-code mappings in one place, documented errors on the fallible public parsers, and simpler option, result and formatting idioms. No change to parsing, writing, numeric conversion, encoding or the public API. Ported from storytold/cadcraft#2571. * craft-dxf: read R12 and earlier binary DXF (1-byte group codes) Binary DXF written before R13 stores each group code in one byte, with 255 escaping a 2-byte code. The reader only knew the R13+ layout, where codes are two bytes, and also treated 255 as an escape there. The layout is now told from the first tag; 255 is an escape only in the 1-byte layout and an ordinary code in R13+. Ported from storytold/cadcraft#1499. * craft-dxf: section_slices, record_slices and Tag::str_ref borrow instead of copying sections() and records() copy every tag out of the parsed list, so a reader that keeps the list holds a large file's tags two or three times. The new functions return slices of the list. sections() and records() now build their copies from them, so there is one implementation of each split. Ported from storytold/cadcraft#1708. storytold/cadcraft#1588 solves the same problem by changing Section itself; this keeps the existing API. * craft-dxf: reals beyond 1e16 or below 1e-10 are written in exponent form; string_groups format_real wrote f64::MAX as 309 digits. Values outside 1e-10..1e16 are now written as AutoCAD writes them (1.0E+20) and read back exactly. string_groups splits text into the 250-character groups long strings are stored in (MAX_STRING_CHARS). Ported from storytold/cadcraft#1790. * craft-dxf: DOS 720/737/775/855/857/860/861/863/864/865/869 and ISO 8859-15 code pages; cheaper unescape and hex $DWGCODEPAGE values DOS720, 737, 775, 855, 857, 860, 861, 863, 864, 865, 869 and ISO8859_15 had no decoder and fell back to Windows-1252. ISO 8859-15 decodes through encoding_rs, the DOS pages through tables checked against the Unicode Consortium mapping files (dos_tables.rs). DOS 864's byte 0x25 is U+066A, so a writer escapes '%' there. U+FFFD is never written as a table byte. A string without an escape moves into its tag without a copy, cstr finds its terminator with one scan, and binary chunks are hex-encoded from a table. Ported from storytold/cadcraft#1271. Its group-code-255 change is carried by the R12 binary port: the 1-byte layout is detected only when the escaped code is one that needs the escape, so an R13+ file starting with code 255 still reads. * craft-dxf: Korean Johab (ANSI_1361) text and \M+4 escapes decode Johab had no decoder: files declaring ANSI_1361 were read as Windows-1252 and \M+4 escapes stayed literal. Hangul syllables are composed from the three 5-bit jamo indices (KS X 1001 annex 3); symbols and Hanja map onto KS X 1001 rows decoded as EUC-KR. Read only: a writer escapes instead. Ported from storytold/cadcraft#1911 (its DOS code pages are covered by the tables from storytold/cadcraft#1271). --------- Co-authored-by: mo <131036780+mbolling@users.noreply.github.com> Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com> Co-authored-by: Cédric Legrand <cedric@legrand-tech.fr> Co-authored-by: valentinevg812-eng <valentinevg812@gmail.com> Co-authored-by: Mads Holten Rasmussen <mads.h.rasmussen@gmail.com> Co-authored-by: LastSkywalker <maksim@sky-tehnol.uk> Co-authored-by: vangel76 <vangel1976@googlemail.com> Co-authored-by: Francesco BV <francesco.esposito@bvitalia.net>
140 lines
7.0 KiB
Rust
140 lines
7.0 KiB
Rust
//! Follow-ups of the crates/dxf audit (#1269): group code 255, the extra DOS and ISO code pages,
|
||
//! `format_real`, and the escape and integer writers that were rewritten without a change of
|
||
//! behaviour.
|
||
|
||
use craft_dxf::{Tag, Value, decode_code_page, format_real, parse, write_ascii, write_binary};
|
||
|
||
#[test]
|
||
fn group_code_255_is_an_ordinary_code_in_binary_dxf() {
|
||
// Code 255 used to be read as an R12 escape (`FF 00 <code>`), so a file with that code could
|
||
// not be read back.
|
||
let tags = vec![Tag::s(255, "x"), Tag::s(0, "EOF")];
|
||
let back = parse(&write_binary(&tags, true)).unwrap();
|
||
assert_eq!(back.len(), 2);
|
||
assert_eq!((back[0].code, back[0].str()), (255, "x".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn more_dos_and_iso_code_pages_decode() {
|
||
// (code page, byte, the character the code page defines there)
|
||
let cases: [(&str, u8, char); 12] = [
|
||
("DOS720", 0x82, 'é'),
|
||
("DOS737", 0x80, 'Α'),
|
||
("DOS775", 0x80, 'Ć'),
|
||
("DOS855", 0x80, 'ђ'),
|
||
("DOS857", 0x8D, 'ı'),
|
||
("DOS860", 0x84, 'ã'),
|
||
("DOS861", 0x8B, 'Ð'),
|
||
("DOS863", 0x84, 'Â'),
|
||
("DOS864", 0x80, '°'),
|
||
("DOS865", 0x9B, 'ø'),
|
||
("DOS869", 0x86, 'Ά'),
|
||
("ISO8859_15", 0xA4, '€'),
|
||
];
|
||
for (name, byte, ch) in cases {
|
||
assert_eq!(decode_code_page(&[byte], name), Some(ch.to_string()), "{name}");
|
||
assert_eq!(decode_code_page(b"A", &name.to_lowercase()), Some("A".into()), "{name}");
|
||
}
|
||
// An unassigned byte is the replacement character, the rest decodes.
|
||
assert_eq!(decode_code_page(b"\xD5A", "DOS857").as_deref(), Some("\u{FFFD}A"));
|
||
}
|
||
|
||
#[test]
|
||
fn dos_code_pages_decode_known_characters() {
|
||
// Four bytes per table with the character the reference mapping gives there (code point in the
|
||
// comment); U+FFFD marks a byte the reference leaves undefined. DOS737, 775, 855, 857, 860, 861, 863,
|
||
// 864, 865 and 869 come from the Unicode Consortium files in
|
||
// https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/ (CP<number>.TXT). DOS720 has no such file:
|
||
// its reference is only Python's `cp720` codec, which maps the bytes below that we decode to U+FFFD
|
||
// (0x80, 0x81, 0x84, 0x86, 0x8D, 0x8E, 0x8F, 0x90) to the C1 controls U+0080..U+0090.
|
||
let tables: [(&str, [(u8, char); 4]); 11] = [
|
||
("DOS720", [(0x82, 'é'), (0x9B, 'ؤ'), (0xB3, '│'), (0xE3, 'ع')]), // U+00E9, U+0624, U+2502, U+0639
|
||
("DOS737", [(0x80, 'Α'), (0xAB, 'τ'), (0xB3, '│'), (0xF3, '≤')]), // U+0391, U+03C4, U+2502, U+2264
|
||
("DOS775", [(0x80, 'Ć'), (0xAB, '½'), (0xB3, '│'), (0xE3, 'Ń')]), // U+0106, U+00BD, U+2502, U+0143
|
||
("DOS855", [(0x80, 'ђ'), (0xA3, 'Б'), (0xB3, '│'), (0xFB, 'ч')]), // U+0452, U+0411, U+2502, U+0447
|
||
("DOS857", [(0x8D, 'ı'), (0xB3, '│'), (0xE3, 'Ò'), (0xD5, '\u{FFFD}')]), // U+0131, U+2502, U+00D2, undefined
|
||
("DOS860", [(0x84, 'ã'), (0x9B, '¢'), (0xB3, '│'), (0xE3, 'π')]), // U+00E3, U+00A2, U+2502, U+03C0
|
||
("DOS861", [(0x8B, 'Ð'), (0xAB, '½'), (0xB3, '│'), (0xFB, '√')]), // U+00D0, U+00BD, U+2502, U+221A
|
||
("DOS863", [(0x84, 'Â'), (0x9B, '¢'), (0xB3, '│'), (0xE3, 'π')]), // U+00C2, U+00A2, U+2502, U+03C0
|
||
("DOS864", [(0x80, '°'), (0xAB, 'ﺙ'), (0xB3, '٣'), (0xFF, '\u{FFFD}')]), // U+00B0, U+FE99, U+0663, undefined
|
||
("DOS865", [(0x9B, 'ø'), (0xAB, '½'), (0xB3, '│'), (0xE3, 'π')]), // U+00F8, U+00BD, U+2502, U+03C0
|
||
("DOS869", [(0x86, 'Ά'), (0x9B, 'ά'), (0xB3, '│'), (0x80, '\u{FFFD}')]), // U+0386, U+03AC, U+2502, undefined
|
||
];
|
||
for (name, cases) in tables {
|
||
for (byte, ch) in cases {
|
||
assert_eq!(decode_code_page(&[byte], name), Some(ch.to_string()), "{name} 0x{byte:02X}");
|
||
}
|
||
}
|
||
// DOS720: the 8 bytes the Python codec maps to C1 controls are unassigned here.
|
||
for byte in [0x80u8, 0x81, 0x84, 0x86, 0x8D, 0x8E, 0x8F, 0x90] {
|
||
assert_eq!(decode_code_page(&[byte], "DOS720").as_deref(), Some("\u{FFFD}"), "DOS720 0x{byte:02X}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ascii_file_in_a_dos_code_page_reads_its_text() {
|
||
let mut dxf = b"0\nSECTION\n2\nHEADER\n9\n$DWGCODEPAGE\n3\nDOS855\n0\nENDSEC\n0\nSECTION\n2\nTABLES\n8\n".to_vec();
|
||
dxf.extend_from_slice(b"\xA1\xA2\n0\nENDSEC\n0\nEOF\n");
|
||
let tags = parse(&dxf).unwrap();
|
||
let layer = tags.iter().find(|t| t.code == 8).unwrap();
|
||
assert_eq!(layer.str(), decode_code_page(b"\xA1\xA2", "DOS855").unwrap());
|
||
assert_eq!(layer.str(), "Аб");
|
||
}
|
||
|
||
#[test]
|
||
fn format_real_always_has_a_point() {
|
||
assert_eq!(format_real(5.0), "5.0");
|
||
assert_eq!(format_real(-0.0), "-0.0");
|
||
assert_eq!(format_real(0.1), "0.1");
|
||
assert_eq!(format_real(1e15), "1000000000000000.0");
|
||
assert_eq!(format_real(1e21), "1.0E+21");
|
||
assert_eq!(format_real(1e-7), "0.0000001");
|
||
assert_eq!(format_real(f64::MAX).parse::<f64>().unwrap(), f64::MAX);
|
||
assert_eq!(format_real(f64::NAN), "0.0");
|
||
assert_eq!(format_real(f64::INFINITY), "0.0");
|
||
assert_eq!(format_real(f64::NEG_INFINITY), "0.0");
|
||
}
|
||
|
||
#[test]
|
||
fn escapes_next_to_other_text_and_backslashes() {
|
||
let one = |s: &str| parse(format!("1\n{s}\n0\nEOF\n").as_bytes()).unwrap()[0].str();
|
||
assert_eq!(one("a\\U+00E9b"), "aéb");
|
||
assert_eq!(one("é\\U+0041\\ ü\\M+5BCFE\\U+D83D\\U+DE00 x\\"), "éA\\ ü件😀 x\\");
|
||
assert_eq!(one("\\\\U+0041"), "\\A");
|
||
assert_eq!(one("\\U+D83D\\U+0041 \\U+DE00"), "\\U+D83DA \\U+DE00");
|
||
assert_eq!(one("日本\\U+00"), "日本\\U+00");
|
||
// A long run of backslashes (hostile) is read in linear time and stays literal.
|
||
let slashes = "\\".repeat(100_000);
|
||
assert_eq!(one(&slashes), slashes);
|
||
// The same strings through the binary reader.
|
||
// Built by hand: the writer escapes a literal `\U+` (#2050), so it would not produce an escape.
|
||
let mut file = b"AutoCAD Binary DXF\r\n\x1a\0".to_vec();
|
||
file.extend_from_slice(&1i16.to_le_bytes());
|
||
file.extend_from_slice("é\\U+0041\\ ü".as_bytes());
|
||
file.push(0);
|
||
file.extend_from_slice(&0i16.to_le_bytes());
|
||
file.extend_from_slice(b"EOF\0");
|
||
assert_eq!(parse(&file).unwrap()[0].str(), "éA\\ ü");
|
||
// And the writer keeps the literal text literal.
|
||
let tags = [Tag::s(1, "é\\U+0041\\ ü"), Tag::s(0, "EOF")];
|
||
assert_eq!(parse(&write_binary(&tags, true)).unwrap()[0].str(), "é\\U+0041\\ ü");
|
||
}
|
||
|
||
#[test]
|
||
fn integers_are_written_in_full() {
|
||
let text = write_ascii(&[Tag::i(70, i64::MIN), Tag::i(90, -7), Tag::i(160, 0), Tag::new(290, Value::Bool(true))]);
|
||
assert_eq!(text, " 70\r\n-9223372036854775808\r\n 90\r\n-7\r\n160\r\n0\r\n290\r\n1\r\n");
|
||
}
|
||
|
||
#[test]
|
||
fn binary_hex_reads_every_byte_in_upper_case() {
|
||
let all: Vec<u8> = (0..=255).collect();
|
||
let mut file = b"AutoCAD Binary DXF\r\n\x1a\0".to_vec();
|
||
file.extend_from_slice(&[0x36, 0x01, 0xFF]);
|
||
file.extend_from_slice(&all[..255]);
|
||
file.extend_from_slice(&[0x00, 0x00, b'E', b'O', b'F', 0]);
|
||
let tags = parse(&file).unwrap();
|
||
let want: String = all[..255].iter().map(|b| format!("{b:02X}")).collect();
|
||
assert_eq!(tags[0].value, Value::Hex(want));
|
||
}
|