mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 11:13:55 +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>
125 lines
5.4 KiB
Rust
125 lines
5.4 KiB
Rust
//! Hex payload conformance is distinct from binary framing capacity: DXF groups 310–319 and
|
||
//! 1004 specify at most 127 payload bytes per chunk, even though a length byte represents 255.
|
||
//! Inspect writer bytes without our parser; accept any compliant partition preserving payload,
|
||
//! code and following-record framing. Oversized binary read fixtures test tolerance only.
|
||
//! Pending writer regressions extend the chunk-size finding in issue #1025.
|
||
//! Public DXF Reference, Group Codes in Numerical Order and Binary DXF Files:
|
||
//! <https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-DXF/files/GUID-3F0380A5-1C15-464D-BC66-2C5F094BCFB9.htm>
|
||
//! <https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-DXF/files/GUID-FC1C3C69-DBC2-49E4-893A-000D6538C0FE.htm>
|
||
|
||
use std::fmt::Write as _;
|
||
|
||
use craft_dxf::{Tag, Value, parse, write_ascii_as, write_binary};
|
||
|
||
const SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0";
|
||
const FOLLOWING_BINARY: &[u8] = b"\x46\0\xf9\xff"; // group 70, signed i16 -7
|
||
|
||
fn payload(length: usize) -> Vec<u8> {
|
||
(0..length).map(|index| u8::try_from(index * 37 % 256).unwrap()).collect()
|
||
}
|
||
|
||
fn hex(bytes: &[u8]) -> String {
|
||
bytes.iter().fold(String::new(), |mut out, byte| {
|
||
let _ = write!(out, "{byte:02X}");
|
||
out
|
||
})
|
||
}
|
||
|
||
fn input(code: i32, bytes: &[u8]) -> [Tag; 2] {
|
||
[Tag::new(code, Value::Hex(hex(bytes))), Tag::i(70, -7)]
|
||
}
|
||
|
||
fn check_ascii(code: i32, bytes: &[u8], utf8: bool) {
|
||
let wire = write_ascii_as(&input(code, bytes), utf8);
|
||
let lines: Vec<_> = wire.lines().collect();
|
||
let (chunks, following) = lines.split_at(lines.len().checked_sub(2).unwrap());
|
||
assert_eq!(following[0].trim(), "70");
|
||
assert_eq!(following[1], "-7");
|
||
assert!(!chunks.is_empty(), "an empty payload must still have a tag");
|
||
let (records, remainder) = chunks.as_chunks::<2>();
|
||
assert!(remainder.is_empty(), "incomplete group/value pair");
|
||
let context = format!("ASCII code={code} bytes={} utf8={utf8}", bytes.len());
|
||
let mut back = Vec::new();
|
||
for record in records {
|
||
assert_eq!(record[0].trim().parse::<i32>().unwrap(), code, "{context}");
|
||
let text = record[1];
|
||
assert!(text.len() <= 254, "{context}: {} hex characters exceed 127 payload bytes", text.len());
|
||
assert!(text.bytes().all(|b| b.is_ascii_hexdigit()), "{context}: invalid hex");
|
||
let (pairs, remainder) = text.as_bytes().as_chunks::<2>();
|
||
assert!(remainder.is_empty(), "{context}: partial byte");
|
||
back.extend(pairs.iter().map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()));
|
||
}
|
||
assert_eq!(back, bytes, "{context}: complete payload and byte order");
|
||
}
|
||
|
||
fn check_binary(code: i32, bytes: &[u8], utf8: bool) {
|
||
let wire = write_binary(&input(code, bytes), utf8);
|
||
let mut chunks = wire.strip_prefix(SENTINEL).unwrap().strip_suffix(FOLLOWING_BINARY).unwrap();
|
||
assert!(!chunks.is_empty(), "an empty payload must still have a tag");
|
||
let context = format!("binary code={code} bytes={} utf8={utf8}", bytes.len());
|
||
let mut back = Vec::new();
|
||
while !chunks.is_empty() {
|
||
let (&[lo, hi, length], rest) = chunks.split_first_chunk::<3>().unwrap();
|
||
assert_eq!(i32::from(u16::from_le_bytes([lo, hi])), code, "{context}");
|
||
assert!(length <= 127, "{context}: {length} bytes exceed the documented group limit");
|
||
let (chunk, next) = rest.split_at(usize::from(length));
|
||
back.extend_from_slice(chunk);
|
||
chunks = next;
|
||
}
|
||
assert_eq!(back, bytes, "{context}: complete payload and byte order");
|
||
}
|
||
|
||
#[test]
|
||
fn conforming_payloads_have_independent_ascii_and_binary_wire_checks() {
|
||
for code in (310..=319).chain([1004]) {
|
||
for length in [0, 1, 2, 126, 127] {
|
||
for utf8 in [false, true] {
|
||
let bytes = payload(length);
|
||
check_ascii(code, &bytes, utf8);
|
||
check_binary(code, &bytes, utf8);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn binary_reader_tolerates_the_full_length_byte_domain() {
|
||
// This is deliberately permissive input, not an assertion that oversized chunks conform
|
||
// or that the writer should produce them. The following record catches framing mistakes.
|
||
for code in (310_u16..=319).chain([1004]) {
|
||
for length in 0..=u8::MAX {
|
||
let bytes = payload(usize::from(length));
|
||
let mut wire = SENTINEL.to_vec();
|
||
wire.extend_from_slice(&code.to_le_bytes());
|
||
wire.push(length);
|
||
wire.extend_from_slice(&bytes);
|
||
wire.extend_from_slice(FOLLOWING_BINARY);
|
||
assert_eq!(parse(&wire).unwrap(), input(i32::from(code), &bytes), "code={code} length={length}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "Confirmed ASCII DXF writer nonconformance: hex payloads above 127 bytes are emitted as one oversized chunk"]
|
||
fn ascii_writer_splits_large_payloads_into_conforming_chunks() {
|
||
for code in (310..=319).chain([1004]) {
|
||
for length in [128, 254, 255, 256, 1024] {
|
||
for utf8 in [false, true] {
|
||
check_ascii(code, &payload(length), utf8);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "Confirmed binary DXF writer nonconformance: 255-byte partitioning exceeds the documented 127-byte group limit"]
|
||
fn binary_writer_splits_large_payloads_into_conforming_chunks() {
|
||
for code in (310..=319).chain([1004]) {
|
||
for length in [128, 254, 255, 256, 1024] {
|
||
for utf8 in [false, true] {
|
||
check_binary(code, &payload(length), utf8);
|
||
}
|
||
}
|
||
}
|
||
}
|