icadkit 0.3.7__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {icadkit-0.3.7 → icadkit-0.3.8}/Cargo.lock +2 -2
- {icadkit-0.3.7 → icadkit-0.3.8}/Cargo.toml +2 -1
- {icadkit-0.3.7 → icadkit-0.3.8}/PKG-INFO +26 -2
- {icadkit-0.3.7 → icadkit-0.3.8}/README.md +25 -1
- {icadkit-0.3.7 → icadkit-0.3.8}/corpus/public.jsonl +8 -8
- icadkit-0.3.8/crates/icad-core/src/container.rs +311 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/lib.rs +4 -2
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/parts.rs +51 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/reader.rs +14 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/views.rs +92 -0
- icadkit-0.3.8/crates/icad-core/tests/container.rs +203 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/document.rs +129 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/lib.rs +1 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/api.md +51 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/changelog.md +96 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/drawing.md +82 -6
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/parts.md +20 -0
- icadkit-0.3.8/docs/shapes.md +103 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/support.md +7 -0
- icadkit-0.3.8/docs/writing.md +189 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/pyproject.toml +2 -0
- icadkit-0.3.8/scripts/bump_version.py +132 -0
- icadkit-0.3.8/scripts/test_versions.py +229 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_artifacts.py +9 -7
- {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_corpus.py +1 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/__init__.py +53 -1
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_core.pyi +27 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_csg_occt.py +183 -39
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_saved_occt.py +28 -21
- icadkit-0.3.8/src/icadkit/_shape_occt.py +71 -0
- icadkit-0.3.8/src/icadkit/container.py +137 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/document.py +20 -0
- icadkit-0.3.8/src/icadkit/drawing.py +1104 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/parts.py +101 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/saved.py +40 -27
- icadkit-0.3.8/src/icadkit/shape.py +693 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/views.py +24 -0
- icadkit-0.3.8/src/icadkit/write.py +1750 -0
- icadkit-0.3.8/tests/drawing_fixtures.py +203 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/fixture_builders.py +2 -0
- icadkit-0.3.8/tests/test_container.py +77 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_drawing.py +266 -3
- icadkit-0.3.8/tests/test_parts_material.py +116 -0
- icadkit-0.3.8/tests/test_shape.py +376 -0
- icadkit-0.3.8/tests/test_write.py +722 -0
- icadkit-0.3.7/src/icadkit/drawing.py +0 -304
- icadkit-0.3.7/tests/drawing_fixtures.py +0 -94
- {icadkit-0.3.7 → icadkit-0.3.8}/COMMERCIAL-LICENSE.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/LICENSE +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/Apache-2.0.txt +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/PolyForm-Noncommercial-1.0.0.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/parasolid-core-MIT.txt +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/rust-dependencies.txt +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/THIRD_PARTY_NOTICES.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/corpus/README.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/corpus/generate.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/Cargo.toml +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/LICENSE +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/build.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/compression.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/document.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/error.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/header.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/limits.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/native.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/parasolid.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/provenance.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/record.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/resource.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/schema.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/trailer.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/tests/header.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/tests/resources.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/Cargo.toml +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/LICENSE +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/brep.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/geometry.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/schema.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/csg.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/license.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/native.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/parameters.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/preview.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/references.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/roadmap.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/saved-bodies.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/trailer.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/docs/viewer.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/scripts/check_license.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/scripts/check_public_tree.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/scripts/test_publication.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_install.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_native_corpus.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/__main__.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_preview_bridge.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer/index.html +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer/viewer.css +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer/viewer.js +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer_io.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/api.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/cli.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/csg.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/errors.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/geometry.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/models.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/native.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/parameters.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/preview.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/py.typed +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/references.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/schema.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/trailer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/viewer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/conftest.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/geometry_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/parameter_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/part_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/preview_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/saved_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_api.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_cli.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_corpus.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_csg.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_distribution.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_extrusion.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_followup_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_geometry.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_inspect.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_legacy_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_mirrors.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_native.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_native_corpus.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_native_round.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_parameters.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_part_profiles.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_part_semantics.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_preview.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_profile_shapes.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_references.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_resource_cli.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_resources.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_saved.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_trailer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_v7_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_v8l4_boundary.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_version_geometry.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_viewer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/trailer_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/viewer_assembly_browser.mjs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/viewer_browser.mjs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/tests/viewer_mirror_browser.mjs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.8}/uv.lock +0 -0
|
@@ -89,7 +89,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
|
89
89
|
|
|
90
90
|
[[package]]
|
|
91
91
|
name = "icad-core"
|
|
92
|
-
version = "0.3.
|
|
92
|
+
version = "0.3.8"
|
|
93
93
|
dependencies = [
|
|
94
94
|
"flate2",
|
|
95
95
|
"parasolid-core",
|
|
@@ -99,7 +99,7 @@ dependencies = [
|
|
|
99
99
|
|
|
100
100
|
[[package]]
|
|
101
101
|
name = "icad-python"
|
|
102
|
-
version = "0.3.
|
|
102
|
+
version = "0.3.8"
|
|
103
103
|
dependencies = [
|
|
104
104
|
"icad-core",
|
|
105
105
|
"parasolid-core",
|
|
@@ -3,7 +3,8 @@ members = ["crates/icad-core", "crates/icad-python"]
|
|
|
3
3
|
resolver = "3"
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version
|
|
6
|
+
# Release version source. Update with: python3 scripts/bump_version.py X.Y.Z
|
|
7
|
+
version = "0.3.8"
|
|
7
8
|
edition = "2024"
|
|
8
9
|
rust-version = "1.88"
|
|
9
10
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: icadkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
@@ -32,7 +32,7 @@ A Rust-based Python reader for **iCAD SX `.icd` files**. Read saved part
|
|
|
32
32
|
structure, coordinate frames, names and selected attributes; inspect native
|
|
33
33
|
box/cylinder parameters; and extract embedded Parasolid geometry.
|
|
34
34
|
|
|
35
|
-
Version **0.3.7**
|
|
35
|
+
Version **0.3.7** added:
|
|
36
36
|
|
|
37
37
|
- [Saved final bodies](docs/saved-bodies.md) on rolling-ball blend surfaces
|
|
38
38
|
and on toroidal faces that reach their axis, through `parasolid-core 0.3.7`
|
|
@@ -88,6 +88,30 @@ The optional OCCT dependency requires glibc 2.31+ on Linux; see
|
|
|
88
88
|
To build from a source checkout or sdist, install Rust 1.88+ and a C linker,
|
|
89
89
|
then run `python -m pip install .` (or `'.[preview]'`).
|
|
90
90
|
|
|
91
|
+
## Updating the package version
|
|
92
|
+
|
|
93
|
+
`[workspace.package].version` in `Cargo.toml` is the single release-version
|
|
94
|
+
source. Both Rust crates inherit it, Maturin derives the Python package version
|
|
95
|
+
from it, and `icadkit.__version__` / the CLI use the installed package metadata.
|
|
96
|
+
With Python 3.11+, Cargo and uv available, update it with one command:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
python3 scripts/bump_version.py 0.3.8
|
|
100
|
+
python3 scripts/bump_version.py --check --tag v0.3.8
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The version above is an example. The command updates the workspace version,
|
|
104
|
+
synchronizes `Cargo.lock` and `uv.lock`, and checks their consistency. On failure
|
|
105
|
+
it restores all three files. Add `--offline` to use cached dependencies. Stable
|
|
106
|
+
`X.Y.Z` and development `X.Y.Z-dev.N` versions are supported; Python metadata
|
|
107
|
+
uses `X.Y.Z.devN` for the latter. `--check` does not modify the source or locks.
|
|
108
|
+
CI checks the same rules and requires release tags to match the source version.
|
|
109
|
+
|
|
110
|
+
The `parasolid-core` and `parasolid-kit` dependency pins have their own versions
|
|
111
|
+
and are updated separately. Historical versions in this README and the
|
|
112
|
+
[changelog](docs/changelog.md) describe when features were introduced; retain
|
|
113
|
+
those numbers and write new release notes when preparing a release.
|
|
114
|
+
|
|
91
115
|
## Part structure, frames and attributes
|
|
92
116
|
|
|
93
117
|
```python
|
|
@@ -4,7 +4,7 @@ A Rust-based Python reader for **iCAD SX `.icd` files**. Read saved part
|
|
|
4
4
|
structure, coordinate frames, names and selected attributes; inspect native
|
|
5
5
|
box/cylinder parameters; and extract embedded Parasolid geometry.
|
|
6
6
|
|
|
7
|
-
Version **0.3.7**
|
|
7
|
+
Version **0.3.7** added:
|
|
8
8
|
|
|
9
9
|
- [Saved final bodies](docs/saved-bodies.md) on rolling-ball blend surfaces
|
|
10
10
|
and on toroidal faces that reach their axis, through `parasolid-core 0.3.7`
|
|
@@ -60,6 +60,30 @@ The optional OCCT dependency requires glibc 2.31+ on Linux; see
|
|
|
60
60
|
To build from a source checkout or sdist, install Rust 1.88+ and a C linker,
|
|
61
61
|
then run `python -m pip install .` (or `'.[preview]'`).
|
|
62
62
|
|
|
63
|
+
## Updating the package version
|
|
64
|
+
|
|
65
|
+
`[workspace.package].version` in `Cargo.toml` is the single release-version
|
|
66
|
+
source. Both Rust crates inherit it, Maturin derives the Python package version
|
|
67
|
+
from it, and `icadkit.__version__` / the CLI use the installed package metadata.
|
|
68
|
+
With Python 3.11+, Cargo and uv available, update it with one command:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
python3 scripts/bump_version.py 0.3.8
|
|
72
|
+
python3 scripts/bump_version.py --check --tag v0.3.8
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The version above is an example. The command updates the workspace version,
|
|
76
|
+
synchronizes `Cargo.lock` and `uv.lock`, and checks their consistency. On failure
|
|
77
|
+
it restores all three files. Add `--offline` to use cached dependencies. Stable
|
|
78
|
+
`X.Y.Z` and development `X.Y.Z-dev.N` versions are supported; Python metadata
|
|
79
|
+
uses `X.Y.Z.devN` for the latter. `--check` does not modify the source or locks.
|
|
80
|
+
CI checks the same rules and requires release tags to match the source version.
|
|
81
|
+
|
|
82
|
+
The `parasolid-core` and `parasolid-kit` dependency pins have their own versions
|
|
83
|
+
and are updated separately. Historical versions in this README and the
|
|
84
|
+
[changelog](docs/changelog.md) describe when features were introduced; retain
|
|
85
|
+
those numbers and write new release notes when preparing a release.
|
|
86
|
+
|
|
63
87
|
## Part structure, frames and attributes
|
|
64
88
|
|
|
65
89
|
```python
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{"expected": {"byte_order": "little", "resources": 0}, "family": "synthetic-framing", "holdout": false, "id": "native-uninterpreted", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "native-uninterpreted.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
2
|
-
{"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
3
|
-
{"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
4
|
-
{"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
5
|
-
{"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
6
|
-
{"expected": {"byte_order": "little", "codes": ["schema.missing_base_schema"], "resources": 1, "status": {"brep": "not_checked", "extraction": "complete", "raw_geometry": "unsupported", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "unknown-schema", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "unknown-schema.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
7
|
-
{"expected": {"byte_order": "little", "codes": ["brep.invalid_reference"], "resources": 1, "status": {"brep": "invalid", "extraction": "complete", "raw_geometry": "complete", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-reference", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-reference.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
8
|
-
{"expected": {"byte_order": "little", "codes": ["compression.invalid"], "resources": 1, "status": {"brep": "not_checked", "extraction": "invalid", "raw_geometry": "not_checked", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-checksum", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-checksum.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "
|
|
1
|
+
{"expected": {"byte_order": "little", "resources": 0}, "family": "synthetic-framing", "holdout": false, "id": "native-uninterpreted", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "native-uninterpreted.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "2623d141699e0a846ac1d745b8eb4761421cd6bea4cef7ffb79545919e66f62a"}
|
|
2
|
+
{"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "90049eb96aa3b89c55a9c912efdfcccf41dbf491ccf6f66af84e9acc5185404d"}
|
|
3
|
+
{"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "a768f5d63db6bb91f3178c52304002b3dde9eaef7fdd0c0bd5c61160cd94d0ec"}
|
|
4
|
+
{"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "222597e831d61fb1b57f78251155c9cda08419056e258333570f514b58327e33"}
|
|
5
|
+
{"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "9e0aaf42f3d23941c1ecadb354ef026abc4bf9b40979b76a36f71ac996825d93"}
|
|
6
|
+
{"expected": {"byte_order": "little", "codes": ["schema.missing_base_schema"], "resources": 1, "status": {"brep": "not_checked", "extraction": "complete", "raw_geometry": "unsupported", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "unknown-schema", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "unknown-schema.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "6e6eb075c99ac512366cb0f2dc049cbd5baf52acafd5916f11fbe349ef74793c"}
|
|
7
|
+
{"expected": {"byte_order": "little", "codes": ["brep.invalid_reference"], "resources": 1, "status": {"brep": "invalid", "extraction": "complete", "raw_geometry": "complete", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-reference", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-reference.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "1f796bff73c5c3a536832edaca09718bd3ecedd5645535809806b4e7aaa03f02"}
|
|
8
|
+
{"expected": {"byte_order": "little", "codes": ["compression.invalid"], "resources": 1, "status": {"brep": "not_checked", "extraction": "invalid", "raw_geometry": "not_checked", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-checksum", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-checksum.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "c4b6299ddd93d618fae769272df983e4f0a20b5d915c853792d7842e0720a0de"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
//! Byte-exact model of the directory-indexed container.
|
|
2
|
+
//!
|
|
3
|
+
//! A [`Container`] keeps the MOD record, the DRW word at `+8`, the saved view
|
|
4
|
+
//! names, every indexed record body and the trailing container verbatim. The
|
|
5
|
+
//! framing that depends on them (directory offsets and lengths, `RES`/`V/W`
|
|
6
|
+
//! length words, the MOD total-word field) is recomputed on serialization, so
|
|
7
|
+
//! an unmodified document reproduces its input byte for byte exactly when its
|
|
8
|
+
//! framing follows the observed rules. Nothing is searched for or repaired.
|
|
9
|
+
|
|
10
|
+
use crate::{ByteOrder, Diagnostic, Document, ErrorKind, InspectError};
|
|
11
|
+
|
|
12
|
+
/// Offset of the MOD word that counts the indexed records in 32-bit words.
|
|
13
|
+
const TOTAL_WORDS_AT: usize = 236;
|
|
14
|
+
|
|
15
|
+
/// One directory-indexed record without its opening frame and end tag.
|
|
16
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
17
|
+
pub struct ContainerRecord {
|
|
18
|
+
/// `RES`, `V/W` or `USR`.
|
|
19
|
+
pub tag: &'static str,
|
|
20
|
+
/// The record's own length word at `+4`. `RES` and `V/W` write their
|
|
21
|
+
/// physical size instead; `USR` writes this word back because old files
|
|
22
|
+
/// store a value that is not the physical size.
|
|
23
|
+
pub declared_words: u32,
|
|
24
|
+
/// Bytes from `+8` (the word shared by every record of one save) up to
|
|
25
|
+
/// the end tag, exclusive. The length is a multiple of four.
|
|
26
|
+
pub body: Vec<u8>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl ContainerRecord {
|
|
30
|
+
/// Physical size in 32-bit words, including the 12 framing bytes.
|
|
31
|
+
///
|
|
32
|
+
/// # Errors
|
|
33
|
+
/// Returns `container.alignment` for a body that is not word aligned and
|
|
34
|
+
/// `container.size` for a record that does not fit a 32-bit word count.
|
|
35
|
+
pub fn words(&self) -> Result<u32, InspectError> {
|
|
36
|
+
if !self.body.len().is_multiple_of(4) {
|
|
37
|
+
return Err(InspectError::format(
|
|
38
|
+
ErrorKind::Invalid,
|
|
39
|
+
"container.alignment",
|
|
40
|
+
0,
|
|
41
|
+
format!(
|
|
42
|
+
"{} body of {} bytes is not word aligned",
|
|
43
|
+
self.tag,
|
|
44
|
+
self.body.len()
|
|
45
|
+
),
|
|
46
|
+
));
|
|
47
|
+
}
|
|
48
|
+
u32::try_from(self.body.len() / 4 + 3).map_err(|_| {
|
|
49
|
+
InspectError::format(
|
|
50
|
+
ErrorKind::LimitExceeded,
|
|
51
|
+
"container.size",
|
|
52
|
+
0,
|
|
53
|
+
format!("{} record exceeds the 32-bit word count", self.tag),
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// The indexed container of one document; see the module documentation.
|
|
60
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
61
|
+
pub struct Container {
|
|
62
|
+
pub byte_order: ByteOrder,
|
|
63
|
+
/// The complete 256-byte MOD record. Bytes 236..240 are replaced by the
|
|
64
|
+
/// recomputed total on serialization; the other fields are copied.
|
|
65
|
+
pub mod_record: [u8; 256],
|
|
66
|
+
/// The DRW word at `+8`, retained verbatim.
|
|
67
|
+
pub drw_word: [u8; 4],
|
|
68
|
+
/// Eight-byte saved view names in directory order, retained verbatim.
|
|
69
|
+
pub view_names: Vec<[u8; 8]>,
|
|
70
|
+
/// `RES`, then one `V/W` per view name, then an optional `USR`.
|
|
71
|
+
pub records: Vec<ContainerRecord>,
|
|
72
|
+
/// Everything after the last indexed record, retained verbatim.
|
|
73
|
+
pub tail: Vec<u8>,
|
|
74
|
+
/// Where the source deviated from the recomputed framing.
|
|
75
|
+
pub diagnostics: Vec<Diagnostic>,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn invalid(code: &'static str, message: impl Into<String>) -> InspectError {
|
|
79
|
+
InspectError::format(ErrorKind::Invalid, code, 0, message)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl Container {
|
|
83
|
+
/// Check the record order, the view-name count and the record sizes.
|
|
84
|
+
fn validate(&self) -> Result<(), InspectError> {
|
|
85
|
+
let mut views = 0;
|
|
86
|
+
for (index, record) in self.records.iter().enumerate() {
|
|
87
|
+
let expected_tag = match (index, record.tag) {
|
|
88
|
+
(0, _) => "RES",
|
|
89
|
+
(_, "USR") if index + 1 == self.records.len() => "USR",
|
|
90
|
+
_ => "V/W",
|
|
91
|
+
};
|
|
92
|
+
if record.tag != expected_tag {
|
|
93
|
+
return Err(invalid(
|
|
94
|
+
"container.record_order",
|
|
95
|
+
format!("record {index} is {}, expected {expected_tag}", record.tag),
|
|
96
|
+
));
|
|
97
|
+
}
|
|
98
|
+
if record.tag == "V/W" {
|
|
99
|
+
views += 1;
|
|
100
|
+
}
|
|
101
|
+
record.words()?;
|
|
102
|
+
}
|
|
103
|
+
if self.records.first().map(|r| r.tag) != Some("RES") {
|
|
104
|
+
return Err(invalid(
|
|
105
|
+
"container.record_order",
|
|
106
|
+
"the first record must be RES",
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
if views != self.view_names.len() {
|
|
110
|
+
return Err(invalid(
|
|
111
|
+
"container.view_names",
|
|
112
|
+
format!(
|
|
113
|
+
"{views} V/W records but {} view names",
|
|
114
|
+
self.view_names.len()
|
|
115
|
+
),
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
Ok(())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Byte length of the DRW record for the current number of views.
|
|
122
|
+
fn drw_bytes(&self) -> usize {
|
|
123
|
+
32 + 16 * self.view_names.len()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// The MOD total: DRW plus every indexed record, in 32-bit words.
|
|
127
|
+
///
|
|
128
|
+
/// # Errors
|
|
129
|
+
/// Returns the validation errors of [`Container::to_bytes`].
|
|
130
|
+
pub fn total_words(&self) -> Result<u32, InspectError> {
|
|
131
|
+
self.validate()?;
|
|
132
|
+
let mut total = u32::try_from(self.drw_bytes() / 4)
|
|
133
|
+
.map_err(|_| invalid("container.size", "too many views"))?;
|
|
134
|
+
for record in &self.records {
|
|
135
|
+
total = total
|
|
136
|
+
.checked_add(record.words()?)
|
|
137
|
+
.ok_or_else(|| invalid("container.size", "indexed records exceed 2^32 words"))?;
|
|
138
|
+
}
|
|
139
|
+
Ok(total)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Serialize with recomputed framing; see the module documentation.
|
|
143
|
+
///
|
|
144
|
+
/// # Errors
|
|
145
|
+
/// Returns `container.record_order` for records out of order,
|
|
146
|
+
/// `container.view_names` when the view names do not match the `V/W`
|
|
147
|
+
/// records, `container.alignment` for an unaligned body and
|
|
148
|
+
/// `container.size` when a count does not fit its field.
|
|
149
|
+
pub fn to_bytes(&self) -> Result<Vec<u8>, InspectError> {
|
|
150
|
+
let total = self.total_words()?;
|
|
151
|
+
let order = self.byte_order;
|
|
152
|
+
let entries = self.view_names.len() + 2;
|
|
153
|
+
let mut out = Vec::with_capacity(256 + total as usize * 4 + self.tail.len());
|
|
154
|
+
let mut mod_record = self.mod_record;
|
|
155
|
+
mod_record[TOTAL_WORDS_AT..TOTAL_WORDS_AT + 4].copy_from_slice(&order.bytes(total));
|
|
156
|
+
out.extend_from_slice(&mod_record);
|
|
157
|
+
out.extend_from_slice(b"DRW0");
|
|
158
|
+
let drw_words = u32::try_from(self.drw_bytes() / 4)
|
|
159
|
+
.map_err(|_| invalid("container.size", "too many views"))?;
|
|
160
|
+
out.extend_from_slice(&order.bytes(drw_words));
|
|
161
|
+
out.extend_from_slice(&self.drw_word);
|
|
162
|
+
let mut starts = Vec::with_capacity(entries);
|
|
163
|
+
let mut lengths = Vec::with_capacity(entries);
|
|
164
|
+
let mut cursor = u32::try_from(256 + self.drw_bytes())
|
|
165
|
+
.map_err(|_| invalid("container.size", "too many views"))?;
|
|
166
|
+
for record in &self.records {
|
|
167
|
+
let words = record.words()?;
|
|
168
|
+
// Directory starts are byte offsets; lengths are 32-bit words.
|
|
169
|
+
starts.push(cursor);
|
|
170
|
+
lengths.push(words);
|
|
171
|
+
cursor = words
|
|
172
|
+
.checked_mul(4)
|
|
173
|
+
.and_then(|bytes| cursor.checked_add(bytes))
|
|
174
|
+
.ok_or_else(|| invalid("container.size", "indexed records exceed 4 GiB"))?;
|
|
175
|
+
}
|
|
176
|
+
// A missing USR is a zero entry; the directory still reserves it.
|
|
177
|
+
starts.resize(entries, 0);
|
|
178
|
+
lengths.resize(entries, 0);
|
|
179
|
+
for value in starts.iter().chain(&lengths) {
|
|
180
|
+
out.extend_from_slice(&order.bytes(*value));
|
|
181
|
+
}
|
|
182
|
+
for name in &self.view_names {
|
|
183
|
+
out.extend_from_slice(name);
|
|
184
|
+
}
|
|
185
|
+
out.extend_from_slice(b"DRW1");
|
|
186
|
+
for record in &self.records {
|
|
187
|
+
out.extend_from_slice(record.tag.as_bytes());
|
|
188
|
+
out.push(b'0');
|
|
189
|
+
let length = if record.tag == "USR" {
|
|
190
|
+
record.declared_words
|
|
191
|
+
} else {
|
|
192
|
+
record.words()?
|
|
193
|
+
};
|
|
194
|
+
out.extend_from_slice(&order.bytes(length));
|
|
195
|
+
out.extend_from_slice(&record.body);
|
|
196
|
+
out.extend_from_slice(record.tag.as_bytes());
|
|
197
|
+
out.push(b'1');
|
|
198
|
+
}
|
|
199
|
+
out.extend_from_slice(&self.tail);
|
|
200
|
+
Ok(out)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
impl Document {
|
|
205
|
+
/// Model the indexed container for byte-exact serialization.
|
|
206
|
+
///
|
|
207
|
+
/// The directory, record ranges and tags were validated when the
|
|
208
|
+
/// document was indexed. A stored MOD total that differs from the
|
|
209
|
+
/// recomputed one is reported as `container.total_words` and is not
|
|
210
|
+
/// copied; the recomputed value is written instead.
|
|
211
|
+
///
|
|
212
|
+
/// # Errors
|
|
213
|
+
/// Returns `container.range` when an indexed range does not lie inside
|
|
214
|
+
/// the input and `container.view_names` when the directory's view names
|
|
215
|
+
/// do not match its `V/W` records.
|
|
216
|
+
pub fn container(&self) -> Result<Container, InspectError> {
|
|
217
|
+
let data = self.all_bytes();
|
|
218
|
+
let order = self.inspection().header.byte_order;
|
|
219
|
+
let slice = |start: usize, end: usize| {
|
|
220
|
+
data.get(start..end).ok_or_else(|| {
|
|
221
|
+
InspectError::format(
|
|
222
|
+
ErrorKind::Invalid,
|
|
223
|
+
"container.range",
|
|
224
|
+
start as u64,
|
|
225
|
+
format!("indexed range [{start}, {end}) exceeds the input"),
|
|
226
|
+
)
|
|
227
|
+
})
|
|
228
|
+
};
|
|
229
|
+
let indexed = self.records();
|
|
230
|
+
let drw = indexed
|
|
231
|
+
.get(1)
|
|
232
|
+
.filter(|record| record.tag == "DRW")
|
|
233
|
+
.ok_or_else(|| invalid("container.range", "the DRW record is not indexed"))?;
|
|
234
|
+
let drw_len = (drw.byte_range.end - drw.byte_range.start) as usize;
|
|
235
|
+
let views = drw_len.saturating_sub(32) / 16;
|
|
236
|
+
let names_at = 268 + 8 * (views + 2);
|
|
237
|
+
let mut view_names = Vec::with_capacity(views);
|
|
238
|
+
for index in 0..views {
|
|
239
|
+
let mut name = [0; 8];
|
|
240
|
+
name.copy_from_slice(slice(names_at + 8 * index, names_at + 8 * index + 8)?);
|
|
241
|
+
view_names.push(name);
|
|
242
|
+
}
|
|
243
|
+
let mut records = Vec::with_capacity(indexed.len().saturating_sub(2));
|
|
244
|
+
let mut last_end = drw.byte_range.end as usize;
|
|
245
|
+
for record in indexed.iter().skip(2) {
|
|
246
|
+
let start = record.byte_range.start as usize;
|
|
247
|
+
let end = record.byte_range.end as usize;
|
|
248
|
+
let length = slice(start + 4, start + 8)?;
|
|
249
|
+
records.push(ContainerRecord {
|
|
250
|
+
tag: record.tag,
|
|
251
|
+
declared_words: order.u32([length[0], length[1], length[2], length[3]]),
|
|
252
|
+
body: slice(start + 8, end - 4)?.to_vec(),
|
|
253
|
+
});
|
|
254
|
+
last_end = end;
|
|
255
|
+
}
|
|
256
|
+
let mut drw_word = [0; 4];
|
|
257
|
+
drw_word.copy_from_slice(slice(264, 268)?);
|
|
258
|
+
let mut container = Container::new(
|
|
259
|
+
order,
|
|
260
|
+
self.inspection().header.raw_mod,
|
|
261
|
+
drw_word,
|
|
262
|
+
view_names,
|
|
263
|
+
records,
|
|
264
|
+
slice(last_end, data.len())?.to_vec(),
|
|
265
|
+
)?;
|
|
266
|
+
let stored = order.u32([
|
|
267
|
+
container.mod_record[TOTAL_WORDS_AT],
|
|
268
|
+
container.mod_record[TOTAL_WORDS_AT + 1],
|
|
269
|
+
container.mod_record[TOTAL_WORDS_AT + 2],
|
|
270
|
+
container.mod_record[TOTAL_WORDS_AT + 3],
|
|
271
|
+
]);
|
|
272
|
+
let total = container.total_words()?;
|
|
273
|
+
if stored != total {
|
|
274
|
+
container.diagnostics.push(Diagnostic {
|
|
275
|
+
kind: ErrorKind::Unsupported,
|
|
276
|
+
code: "container.total_words",
|
|
277
|
+
byte_offset: TOTAL_WORDS_AT as u64,
|
|
278
|
+
message: format!("MOD stores {stored} words but the indexed records span {total}"),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
Ok(container)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
impl Container {
|
|
286
|
+
/// Assemble a container from its retained parts and validate the record
|
|
287
|
+
/// order, the view-name count and the record alignment.
|
|
288
|
+
///
|
|
289
|
+
/// # Errors
|
|
290
|
+
/// Returns the validation errors of [`Container::to_bytes`].
|
|
291
|
+
pub fn new(
|
|
292
|
+
byte_order: ByteOrder,
|
|
293
|
+
mod_record: [u8; 256],
|
|
294
|
+
drw_word: [u8; 4],
|
|
295
|
+
view_names: Vec<[u8; 8]>,
|
|
296
|
+
records: Vec<ContainerRecord>,
|
|
297
|
+
tail: Vec<u8>,
|
|
298
|
+
) -> Result<Self, InspectError> {
|
|
299
|
+
let container = Self {
|
|
300
|
+
byte_order,
|
|
301
|
+
mod_record,
|
|
302
|
+
drw_word,
|
|
303
|
+
view_names,
|
|
304
|
+
records,
|
|
305
|
+
tail,
|
|
306
|
+
diagnostics: Vec::new(),
|
|
307
|
+
};
|
|
308
|
+
container.validate()?;
|
|
309
|
+
Ok(container)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//! Bounded iCAD SX framing, structural resource indexing and lazy extraction.
|
|
2
2
|
|
|
3
3
|
mod compression;
|
|
4
|
+
mod container;
|
|
4
5
|
mod document;
|
|
5
6
|
mod error;
|
|
6
7
|
mod header;
|
|
@@ -16,6 +17,7 @@ mod schema;
|
|
|
16
17
|
mod trailer;
|
|
17
18
|
mod views;
|
|
18
19
|
|
|
20
|
+
pub use container::{Container, ContainerRecord};
|
|
19
21
|
pub use document::Document;
|
|
20
22
|
pub use error::{Diagnostic, ErrorKind, InspectError};
|
|
21
23
|
pub use header::{
|
|
@@ -28,8 +30,8 @@ pub use parasolid::{
|
|
|
28
30
|
GeometryDiagnostic, GeometryError, GeometryResult, GeometryStatus, SchemaSelection,
|
|
29
31
|
};
|
|
30
32
|
pub use parts::{
|
|
31
|
-
PartAttributeRecord, PartIndex, PartLimits,
|
|
32
|
-
PartViewRecord,
|
|
33
|
+
PartAttributeRecord, PartIndex, PartLimits, PartMaterialRecord, PartOpaqueRange,
|
|
34
|
+
PartProfileInfo, PartRecord, PartViewRecord,
|
|
33
35
|
};
|
|
34
36
|
pub use provenance::{Extraction, SourceRef};
|
|
35
37
|
pub use reader::ByteOrder;
|
|
@@ -40,12 +40,26 @@ pub struct PartRecord {
|
|
|
40
40
|
pub extra_fields: Vec<PartTextRecord>,
|
|
41
41
|
pub opaque_attributes: Vec<PartAttributeRecord>,
|
|
42
42
|
pub entities: Vec<crate::NativeEntityRecord>,
|
|
43
|
+
/// Materials of the metadata blocks that follow this part record, one per
|
|
44
|
+
/// saved body; see [`PartMaterialRecord`].
|
|
45
|
+
pub materials: Vec<PartMaterialRecord>,
|
|
43
46
|
pub parent_source_id: u32,
|
|
44
47
|
pub first_child_source_id: u32,
|
|
45
48
|
pub previous_source_id: u32,
|
|
46
49
|
pub next_source_id: u32,
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
/// A saved material subrecord (tag `0x7a`, 216 bytes) of the metadata block
|
|
53
|
+
/// that follows its owner's part record: CP932 name, material identifier
|
|
54
|
+
/// and specific gravity. Text is retained undecoded.
|
|
55
|
+
#[derive(Debug, Clone)]
|
|
56
|
+
pub struct PartMaterialRecord {
|
|
57
|
+
pub byte_range: ByteRange,
|
|
58
|
+
pub raw_name: Vec<u8>,
|
|
59
|
+
pub raw_material_id: Vec<u8>,
|
|
60
|
+
pub specific_gravity: f64,
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
#[derive(Debug, Clone)]
|
|
50
64
|
pub struct PartTextRecord {
|
|
51
65
|
pub byte_range: ByteRange,
|
|
@@ -105,6 +119,38 @@ fn word(bytes: &[u8], at: usize) -> u32 {
|
|
|
105
119
|
u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
|
|
106
120
|
}
|
|
107
121
|
|
|
122
|
+
/// Collect the material subrecords of one qualified metadata block. The
|
|
123
|
+
/// block's framing was validated by `metadata_layout`; every subrecord with
|
|
124
|
+
/// tag `0x7a` and the observed 216-byte length is a material of the part
|
|
125
|
+
/// whose record precedes the block. Other tags and lengths stay opaque.
|
|
126
|
+
fn collect_materials(part: &mut PartRecord, block: &[u8], base: u64) {
|
|
127
|
+
let count = word(block, 8) as usize;
|
|
128
|
+
let mut at = 160;
|
|
129
|
+
for _ in 0..count {
|
|
130
|
+
if block.len() - at < 4 {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
let tag = word(block, at);
|
|
134
|
+
let length = (tag & 0x00ff_ffff) as usize;
|
|
135
|
+
if length < 4 || length > block.len() - at {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
if tag >> 24 == 0x7a && length == 216 {
|
|
139
|
+
let sub = &block[at..at + length];
|
|
140
|
+
part.materials.push(PartMaterialRecord {
|
|
141
|
+
byte_range: ByteRange {
|
|
142
|
+
start: base + at as u64,
|
|
143
|
+
end: base + (at + length) as u64,
|
|
144
|
+
},
|
|
145
|
+
raw_name: sub[16..96].to_vec(),
|
|
146
|
+
raw_material_id: sub[96..136].to_vec(),
|
|
147
|
+
specific_gravity: f64::from_le_bytes(std::array::from_fn(|i| sub[136 + i])),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
at += length;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
108
154
|
fn limit(at: u64, code: &'static str, message: &str) -> InspectError {
|
|
109
155
|
InspectError::format(ErrorKind::LimitExceeded, code, at, message)
|
|
110
156
|
}
|
|
@@ -722,6 +768,9 @@ impl Document {
|
|
|
722
768
|
}
|
|
723
769
|
entities += metadata_entities;
|
|
724
770
|
result.opaque(base + at as u64, base + next as u64, "entity_metadata");
|
|
771
|
+
if let Some(part) = result.parts.last_mut() {
|
|
772
|
+
collect_materials(part, &bytes[at..next], base + at as u64);
|
|
773
|
+
}
|
|
725
774
|
at = next;
|
|
726
775
|
continue;
|
|
727
776
|
}
|
|
@@ -907,6 +956,7 @@ impl Document {
|
|
|
907
956
|
extra_fields: Vec::new(),
|
|
908
957
|
opaque_attributes: Vec::new(),
|
|
909
958
|
entities: Vec::new(),
|
|
959
|
+
materials: Vec::new(),
|
|
910
960
|
parent_source_id: word(bytes, at + 260),
|
|
911
961
|
first_child_source_id: word(bytes, at + 264),
|
|
912
962
|
previous_source_id: word(bytes, at + 268),
|
|
@@ -1230,6 +1280,7 @@ impl Document {
|
|
|
1230
1280
|
extra_fields: Vec::new(),
|
|
1231
1281
|
opaque_attributes: Vec::new(),
|
|
1232
1282
|
entities: Vec::new(),
|
|
1283
|
+
materials: Vec::new(),
|
|
1233
1284
|
parent_source_id: u(260),
|
|
1234
1285
|
first_child_source_id: u(264),
|
|
1235
1286
|
previous_source_id: u(268),
|
|
@@ -23,6 +23,13 @@ impl ByteOrder {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
pub(crate) fn bytes(self, value: u32) -> [u8; 4] {
|
|
27
|
+
match self {
|
|
28
|
+
Self::Little => value.to_le_bytes(),
|
|
29
|
+
Self::Big => value.to_be_bytes(),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
pub(crate) fn u16(self, bytes: [u8; 2]) -> u16 {
|
|
27
34
|
match self {
|
|
28
35
|
Self::Little => u16::from_le_bytes(bytes),
|
|
@@ -30,6 +37,13 @@ impl ByteOrder {
|
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
pub(crate) fn f32(self, bytes: [u8; 4]) -> f32 {
|
|
41
|
+
match self {
|
|
42
|
+
Self::Little => f32::from_le_bytes(bytes),
|
|
43
|
+
Self::Big => f32::from_be_bytes(bytes),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
pub(crate) fn f64(self, bytes: [u8; 8]) -> f64 {
|
|
34
48
|
match self {
|
|
35
49
|
Self::Little => f64::from_le_bytes(bytes),
|