mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +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>
153 lines
5.8 KiB
Rust
153 lines
5.8 KiB
Rust
//! Deterministic tag preservation through the public byte API, in both encodings and every
|
||
//! supported version. Inputs have the group code's type and representable values; lossy
|
||
//! coercions are not accepted as round-trip equality. Real values must retain their bits.
|
||
|
||
use craft_dxf::{Tag, Value, Version, is_binary, parse, write_ascii_as, write_binary};
|
||
|
||
fn check(case: &str, body: &[Tag]) {
|
||
for version in Version::ALL {
|
||
let mut original = vec![
|
||
Tag::s(0, "SECTION"),
|
||
Tag::s(2, "HEADER"),
|
||
Tag::s(9, "$ACADVER"),
|
||
Tag::s(1, version.code()),
|
||
Tag::s(9, "$DWGCODEPAGE"),
|
||
Tag::s(3, "ANSI_1252"),
|
||
Tag::s(0, "ENDSEC"),
|
||
Tag::s(0, "SECTION"),
|
||
Tag::s(2, "ENTITIES"),
|
||
];
|
||
original.extend_from_slice(body);
|
||
original.extend([Tag::s(0, "ENDSEC"), Tag::s(0, "EOF")]);
|
||
for binary in [false, true] {
|
||
let mut current = original.clone();
|
||
for pass in 1..=2 {
|
||
let context = format!("{case} · {version} · binary={binary} · pass={pass}");
|
||
let bytes = if binary { write_binary(¤t, version.utf8()) } else { write_ascii_as(¤t, version.utf8()).into_bytes() };
|
||
assert_eq!(is_binary(&bytes), binary, "{context}");
|
||
let back = parse(&bytes).unwrap_or_else(|e| panic!("{context}: {e}"));
|
||
assert_eq!(back.len(), original.len(), "{context}: tag count");
|
||
for (index, (expected, actual)) in original.iter().zip(&back).enumerate() {
|
||
assert_eq!(actual, expected, "{context}: tag[{index}]");
|
||
if let (Value::Real(a), Value::Real(b)) = (&expected.value, &actual.value) {
|
||
assert_eq!(b.to_bits(), a.to_bits(), "{context}: tag[{index}] real bits");
|
||
}
|
||
}
|
||
assert_eq!(back, current, "{context}: no additional changes on resave");
|
||
current = back;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn integer_width_boundaries() {
|
||
// Explicit reference ranges, not obtained from the codec's kind_of().
|
||
let widths = [
|
||
(vec![60..=79, 170..=179, 270..=289, 370..=389, 400..=409, 1060..=1070], i64::from(i16::MIN), i64::from(i16::MAX)),
|
||
(vec![90..=99, 420..=429, 440..=459, 1071..=1071], i64::from(i32::MIN), i64::from(i32::MAX)),
|
||
(std::iter::once(160..=169).collect(), i64::MIN, i64::MAX),
|
||
];
|
||
for (ranges, min, max) in widths {
|
||
for value in [min, min + 1, -1, 0, 1, max - 1, max] {
|
||
let tags: Vec<_> = ranges.iter().cloned().flatten().map(|code| Tag::i(code, value)).collect();
|
||
check(&format!("integer/{min}..{max}/{value}"), &tags);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn finite_reals_keep_all_bits() {
|
||
let values = [
|
||
0.0,
|
||
-0.0,
|
||
f64::from_bits(1),
|
||
-f64::from_bits(1),
|
||
f64::MIN_POSITIVE,
|
||
f64::MIN_POSITIVE.next_down(),
|
||
0.1,
|
||
-3.25,
|
||
1.0_f64.next_down(),
|
||
1.0,
|
||
1.0_f64.next_up(),
|
||
1e-100,
|
||
-1e100,
|
||
f64::MAX,
|
||
f64::MIN,
|
||
];
|
||
for value in values {
|
||
let tags: Vec<_> =
|
||
[10..=59, 110..=149, 210..=239, 460..=469, 1010..=1059].into_iter().flatten().map(|code| Tag::new(code, Value::Real(value))).collect();
|
||
check(&format!("real/{:016X}", value.to_bits()), &tags);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn booleans() {
|
||
for value in [false, true] {
|
||
let tags: Vec<_> = (290..=299).map(|code| Tag::new(code, Value::Bool(value))).collect();
|
||
check(&format!("bool/{value}"), &tags);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn strings_preserve_whitespace_unicode_and_literal_escapes() {
|
||
// No CR/LF or NUL: those are line/string delimiters, not losslessly writable payloads.
|
||
let values = [
|
||
("empty", ""),
|
||
("spaces", " leading and trailing "),
|
||
("tabs", "\tindented\t"),
|
||
("unicode", "Café Ø 图号 Україна 😀 e\u{301}"),
|
||
("literal-escapes", r"\U+0041 \M+183A4 \U+D83D\U+DE00 \\U+005C \P %%c"),
|
||
("malformed-escapes", r"\U+ \U+ZZZZ \U+D800 \U+DC00 \M+9FFFF"),
|
||
];
|
||
for (case, value) in values {
|
||
let tags: Vec<_> =
|
||
[0..=9, 100..=100, 102..=102, 105..=105, 300..=309, 320..=369, 410..=419, 430..=439, 470..=479, 480..=481, 1000..=1003, 1005..=1005]
|
||
.into_iter()
|
||
.flatten()
|
||
.map(|code| Tag::s(code, value))
|
||
.collect();
|
||
check(case, &tags);
|
||
}
|
||
for length in [1, 254, 255, 256, 1024] {
|
||
check(&format!("string/length={length}"), &[Tag::s(1, "x".repeat(length))]);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn hex_chunks_keep_bytes_and_order() {
|
||
// DXF groups 310–319 and 1004 permit 127 payload bytes per chunk. A binary length
|
||
// byte can represent more, but that is reader tolerance, not writer conformance.
|
||
// Larger payloads need chunk-size/reassembly checks rather than exact tag equality;
|
||
// see hex_chunks.rs for the independent wire checks and pending writer regressions.
|
||
for length in [0, 1, 2, 126, 127] {
|
||
let hex: String = (0..length).fold(String::new(), |mut out, i| {
|
||
use std::fmt::Write as _;
|
||
let _ = write!(out, "{:02X}", (i * 37) % 256);
|
||
out
|
||
});
|
||
let tags: Vec<_> = (310..=319).chain([1004]).map(|code| Tag::new(code, Value::Hex(hex.clone()))).collect();
|
||
check(&format!("hex/bytes={length}"), &tags);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn mixed_values_and_repeated_codes_keep_order() {
|
||
check(
|
||
"mixed/repeated-codes",
|
||
&[
|
||
Tag::s(0, "TEST"),
|
||
Tag::f(10, 1.25),
|
||
Tag::s(1, "first"),
|
||
Tag::f(10, -2.5),
|
||
Tag::i(70, -7),
|
||
Tag::s(1, "second"),
|
||
Tag::new(290, Value::Bool(true)),
|
||
Tag::new(310, Value::Hex("00FF80".into())),
|
||
Tag::i(160, i64::MAX),
|
||
Tag::s(1, ""),
|
||
],
|
||
);
|
||
}
|