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.
Files changed (153) hide show
  1. {icadkit-0.3.7 → icadkit-0.3.8}/Cargo.lock +2 -2
  2. {icadkit-0.3.7 → icadkit-0.3.8}/Cargo.toml +2 -1
  3. {icadkit-0.3.7 → icadkit-0.3.8}/PKG-INFO +26 -2
  4. {icadkit-0.3.7 → icadkit-0.3.8}/README.md +25 -1
  5. {icadkit-0.3.7 → icadkit-0.3.8}/corpus/public.jsonl +8 -8
  6. icadkit-0.3.8/crates/icad-core/src/container.rs +311 -0
  7. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/lib.rs +4 -2
  8. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/parts.rs +51 -0
  9. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/reader.rs +14 -0
  10. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/views.rs +92 -0
  11. icadkit-0.3.8/crates/icad-core/tests/container.rs +203 -0
  12. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/document.rs +129 -0
  13. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/lib.rs +1 -0
  14. {icadkit-0.3.7 → icadkit-0.3.8}/docs/api.md +51 -0
  15. {icadkit-0.3.7 → icadkit-0.3.8}/docs/changelog.md +96 -0
  16. {icadkit-0.3.7 → icadkit-0.3.8}/docs/drawing.md +82 -6
  17. {icadkit-0.3.7 → icadkit-0.3.8}/docs/parts.md +20 -0
  18. icadkit-0.3.8/docs/shapes.md +103 -0
  19. {icadkit-0.3.7 → icadkit-0.3.8}/docs/support.md +7 -0
  20. icadkit-0.3.8/docs/writing.md +189 -0
  21. {icadkit-0.3.7 → icadkit-0.3.8}/pyproject.toml +2 -0
  22. icadkit-0.3.8/scripts/bump_version.py +132 -0
  23. icadkit-0.3.8/scripts/test_versions.py +229 -0
  24. {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_artifacts.py +9 -7
  25. {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_corpus.py +1 -0
  26. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/__init__.py +53 -1
  27. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_core.pyi +27 -0
  28. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_csg_occt.py +183 -39
  29. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_saved_occt.py +28 -21
  30. icadkit-0.3.8/src/icadkit/_shape_occt.py +71 -0
  31. icadkit-0.3.8/src/icadkit/container.py +137 -0
  32. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/document.py +20 -0
  33. icadkit-0.3.8/src/icadkit/drawing.py +1104 -0
  34. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/parts.py +101 -0
  35. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/saved.py +40 -27
  36. icadkit-0.3.8/src/icadkit/shape.py +693 -0
  37. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/views.py +24 -0
  38. icadkit-0.3.8/src/icadkit/write.py +1750 -0
  39. icadkit-0.3.8/tests/drawing_fixtures.py +203 -0
  40. {icadkit-0.3.7 → icadkit-0.3.8}/tests/fixture_builders.py +2 -0
  41. icadkit-0.3.8/tests/test_container.py +77 -0
  42. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_drawing.py +266 -3
  43. icadkit-0.3.8/tests/test_parts_material.py +116 -0
  44. icadkit-0.3.8/tests/test_shape.py +376 -0
  45. icadkit-0.3.8/tests/test_write.py +722 -0
  46. icadkit-0.3.7/src/icadkit/drawing.py +0 -304
  47. icadkit-0.3.7/tests/drawing_fixtures.py +0 -94
  48. {icadkit-0.3.7 → icadkit-0.3.8}/COMMERCIAL-LICENSE.md +0 -0
  49. {icadkit-0.3.7 → icadkit-0.3.8}/LICENSE +0 -0
  50. {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/Apache-2.0.txt +0 -0
  51. {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/PolyForm-Noncommercial-1.0.0.md +0 -0
  52. {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/parasolid-core-MIT.txt +0 -0
  53. {icadkit-0.3.7 → icadkit-0.3.8}/LICENSES/rust-dependencies.txt +0 -0
  54. {icadkit-0.3.7 → icadkit-0.3.8}/THIRD_PARTY_NOTICES.md +0 -0
  55. {icadkit-0.3.7 → icadkit-0.3.8}/corpus/README.md +0 -0
  56. {icadkit-0.3.7 → icadkit-0.3.8}/corpus/generate.py +0 -0
  57. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/Cargo.toml +0 -0
  58. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/LICENSE +0 -0
  59. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/build.rs +0 -0
  60. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/compression.rs +0 -0
  61. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/document.rs +0 -0
  62. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/error.rs +0 -0
  63. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/header.rs +0 -0
  64. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/limits.rs +0 -0
  65. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/native.rs +0 -0
  66. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/parasolid.rs +0 -0
  67. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/provenance.rs +0 -0
  68. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/record.rs +0 -0
  69. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/resource.rs +0 -0
  70. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/schema.rs +0 -0
  71. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/src/trailer.rs +0 -0
  72. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/tests/header.rs +0 -0
  73. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-core/tests/resources.rs +0 -0
  74. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/Cargo.toml +0 -0
  75. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/LICENSE +0 -0
  76. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/brep.rs +0 -0
  77. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/geometry.rs +0 -0
  78. {icadkit-0.3.7 → icadkit-0.3.8}/crates/icad-python/src/schema.rs +0 -0
  79. {icadkit-0.3.7 → icadkit-0.3.8}/docs/csg.md +0 -0
  80. {icadkit-0.3.7 → icadkit-0.3.8}/docs/license.md +0 -0
  81. {icadkit-0.3.7 → icadkit-0.3.8}/docs/native.md +0 -0
  82. {icadkit-0.3.7 → icadkit-0.3.8}/docs/parameters.md +0 -0
  83. {icadkit-0.3.7 → icadkit-0.3.8}/docs/preview.md +0 -0
  84. {icadkit-0.3.7 → icadkit-0.3.8}/docs/references.md +0 -0
  85. {icadkit-0.3.7 → icadkit-0.3.8}/docs/roadmap.md +0 -0
  86. {icadkit-0.3.7 → icadkit-0.3.8}/docs/saved-bodies.md +0 -0
  87. {icadkit-0.3.7 → icadkit-0.3.8}/docs/trailer.md +0 -0
  88. {icadkit-0.3.7 → icadkit-0.3.8}/docs/viewer.md +0 -0
  89. {icadkit-0.3.7 → icadkit-0.3.8}/scripts/check_license.py +0 -0
  90. {icadkit-0.3.7 → icadkit-0.3.8}/scripts/check_public_tree.py +0 -0
  91. {icadkit-0.3.7 → icadkit-0.3.8}/scripts/test_publication.py +0 -0
  92. {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_install.py +0 -0
  93. {icadkit-0.3.7 → icadkit-0.3.8}/scripts/verify_native_corpus.py +0 -0
  94. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/__main__.py +0 -0
  95. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_preview_bridge.py +0 -0
  96. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer/index.html +0 -0
  97. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer/viewer.css +0 -0
  98. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer/viewer.js +0 -0
  99. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/_viewer_io.py +0 -0
  100. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/api.py +0 -0
  101. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/cli.py +0 -0
  102. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/csg.py +0 -0
  103. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/errors.py +0 -0
  104. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/geometry.py +0 -0
  105. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/models.py +0 -0
  106. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/native.py +0 -0
  107. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/parameters.py +0 -0
  108. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/preview.py +0 -0
  109. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/py.typed +0 -0
  110. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/references.py +0 -0
  111. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/schema.py +0 -0
  112. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/trailer.py +0 -0
  113. {icadkit-0.3.7 → icadkit-0.3.8}/src/icadkit/viewer.py +0 -0
  114. {icadkit-0.3.7 → icadkit-0.3.8}/tests/conftest.py +0 -0
  115. {icadkit-0.3.7 → icadkit-0.3.8}/tests/geometry_fixtures.py +0 -0
  116. {icadkit-0.3.7 → icadkit-0.3.8}/tests/parameter_fixtures.py +0 -0
  117. {icadkit-0.3.7 → icadkit-0.3.8}/tests/part_fixtures.py +0 -0
  118. {icadkit-0.3.7 → icadkit-0.3.8}/tests/preview_fixtures.py +0 -0
  119. {icadkit-0.3.7 → icadkit-0.3.8}/tests/saved_fixtures.py +0 -0
  120. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_api.py +0 -0
  121. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_cli.py +0 -0
  122. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_corpus.py +0 -0
  123. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_csg.py +0 -0
  124. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_distribution.py +0 -0
  125. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_extrusion.py +0 -0
  126. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_followup_parts.py +0 -0
  127. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_geometry.py +0 -0
  128. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_inspect.py +0 -0
  129. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_legacy_parts.py +0 -0
  130. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_mirrors.py +0 -0
  131. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_native.py +0 -0
  132. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_native_corpus.py +0 -0
  133. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_native_round.py +0 -0
  134. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_parameters.py +0 -0
  135. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_part_profiles.py +0 -0
  136. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_part_semantics.py +0 -0
  137. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_parts.py +0 -0
  138. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_preview.py +0 -0
  139. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_profile_shapes.py +0 -0
  140. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_references.py +0 -0
  141. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_resource_cli.py +0 -0
  142. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_resources.py +0 -0
  143. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_saved.py +0 -0
  144. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_trailer.py +0 -0
  145. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_v7_parts.py +0 -0
  146. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_v8l4_boundary.py +0 -0
  147. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_version_geometry.py +0 -0
  148. {icadkit-0.3.7 → icadkit-0.3.8}/tests/test_viewer.py +0 -0
  149. {icadkit-0.3.7 → icadkit-0.3.8}/tests/trailer_fixtures.py +0 -0
  150. {icadkit-0.3.7 → icadkit-0.3.8}/tests/viewer_assembly_browser.mjs +0 -0
  151. {icadkit-0.3.7 → icadkit-0.3.8}/tests/viewer_browser.mjs +0 -0
  152. {icadkit-0.3.7 → icadkit-0.3.8}/tests/viewer_mirror_browser.mjs +0 -0
  153. {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.7"
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.7"
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 = "0.3.7"
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.7
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** adds:
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** adds:
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": "d4bb2e63c5cccdac66488f8679740b5962713b9d7dbee0e252f584b722958fc7"}
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": "75102af13dca3d14d69f268b16fc2889242cb23e64365f36b7459c570e9a85bf"}
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": "a2ef56b2ba29e11ecfd838f41e6d87c7cc6587123b4ef7832a45b3bb10a53d2b"}
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": "68ed994d8917e553b59381eb1d56874c4fc7243080fd0b45cee027066bf7829f"}
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": "c18e05dea1a40493193a7662557663f5b17b8d6204bbe1ce03b3c43ceceaecc9"}
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": "18df3e23fa400bc197b9f13c99b280682d92d05bcbbe9bfa0d12fd185996977d"}
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": "c684eb6d0935eb7693386e8221d08a73378414a2c80ce24deeb8929a533f962d"}
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": "cc4e1cdf103193a503db080b0e694865bfaff133ef198c7c41460f093c90e426"}
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, PartOpaqueRange, PartProfileInfo, PartRecord,
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),