Source code for polyxios.validate
import numpy as np
from polyxios._element_types import (
MAX_IMPLIED_VERTICES,
MAX_SAFE_CONN,
MAX_SAFE_ELEMENTS,
MAX_SAFE_VERTICES,
)
from polyxios._types import PolyData
from polyxios.exceptions import ValidationError
[docs]
def validate(poly: PolyData) -> PolyData:
"""Check structural integrity of a PolyData. Raise ValidationError on any violation.
Parameters
----------
poly
PolyData to validate.
Returns
-------
PolyData
The input unchanged if valid (allows use in pipelines).
Raises
------
ValidationError
On dtype mismatch, shape mismatch, out-of-bounds indices, or attribute
length mismatches.
"""
if poly.vertices.ndim != 2 or poly.vertices.shape[1] != 3:
raise ValidationError(
f"vertices must be shape (n, 3), got {poly.vertices.shape}"
)
if poly.vertices.dtype != np.float64:
raise ValidationError(f"vertices must be float64, got {poly.vertices.dtype}")
n_verts = poly.vertices.shape[0]
n_elems = poly.element_types.shape[0]
if poly.element_types.dtype != np.uint8:
raise ValidationError(
f"element_types must be uint8, got {poly.element_types.dtype}"
)
if poly.element_types.ndim != 1:
raise ValidationError("element_types must be 1-D")
if poly.offsets.ndim != 1:
raise ValidationError("offsets must be 1-D")
if poly.offsets.shape[0] != n_elems + 1:
raise ValidationError(
f"offsets length must be n_elements+1={n_elems + 1}, "
f"got {poly.offsets.shape[0]}"
)
if poly.offsets.dtype not in (np.int32, np.int64):
raise ValidationError(
f"offsets must be int32 or int64, got {poly.offsets.dtype}"
)
if poly.connectivity.ndim != 1:
raise ValidationError("connectivity must be 1-D")
if poly.connectivity.dtype not in (np.int32, np.int64):
raise ValidationError(
f"connectivity must be int32 or int64, got {poly.connectivity.dtype}"
)
if n_elems > 0 and poly.connectivity.size > 0:
max_idx = int(poly.connectivity.max())
if max_idx >= n_verts:
raise ValidationError(
f"connectivity contains index {max_idx} but n_verts={n_verts}"
)
min_idx = int(poly.connectivity.min())
if min_idx < 0:
raise ValidationError(f"connectivity contains negative index {min_idx}")
for name, arr in poly.vertex_attrs.items():
if len(arr) != n_verts:
raise ValidationError(
f"vertex_attrs['{name}'] length {len(arr)} != n_verts {n_verts}"
)
for name, arr in poly.element_attrs.items():
if len(arr) != n_elems:
raise ValidationError(
f"element_attrs['{name}'] length {len(arr)} != n_elements {n_elems}"
)
return poly
def validate_header(
declared_n_verts: int,
declared_n_elems: int,
declared_conn_size: int,
file_size_bytes: int,
*,
compressed: bool = False,
spells_vertices: bool = True,
spells_connectivity: bool = True,
) -> None:
"""Validate header counts against file size before any array allocation.
Parameters
----------
declared_n_verts
Number of vertices declared in the file header.
declared_n_elems
Number of elements declared in the file header.
declared_conn_size
Total connectivity size declared in the file header.
file_size_bytes
Actual file size in bytes.
compressed
If True, skip file-size plausibility checks (compressed data is
smaller than the raw vertex/connectivity byte estimates).
spells_vertices
Whether the file holds a coordinate for every vertex it declares.
False for a format that describes its points instead of writing
them - an ImageData spells an origin and a step, a RectilinearGrid
three axes - where the count comes from a header a few bytes long
and no amount of file is evidence for or against it.
spells_connectivity
Whether the file holds the indices it declares. False for the
structured formats, whose cells are implied by the extent and
written nowhere.
Raises
------
ValidationError
If declared counts exceed hard caps or are implausible given file size.
Notes
-----
The caps apply whatever the file spells: they bound what is about to be
allocated, which is the expanded mesh either way. Only the two size
heuristics are gated, and they are heuristics about bytes on disk - a
``4 x 4 x 4`` ImageData declares 64 points in a 231-byte file and is not
corrupt, it is the format working as intended.
A file that spells no points is held to the tighter
:data:`MAX_IMPLIED_VERTICES` in place of the heuristic it is excused from.
The heuristic is what makes the loose cap safe elsewhere: a format that
writes its points has to spend bytes on each one, so a header can only ask
for as much memory as the file it sits in is long. A header that describes
its points instead spends six indices on any number of them, and nothing
stands between that number and the allocation.
"""
if declared_n_verts > MAX_SAFE_VERTICES:
raise ValidationError(
f"declared_n_verts={declared_n_verts} exceeds MAX_SAFE_VERTICES="
f"{MAX_SAFE_VERTICES}. Possible corrupt or malicious file."
)
if not spells_vertices and declared_n_verts > MAX_IMPLIED_VERTICES:
raise ValidationError(
f"declared_n_verts={declared_n_verts} exceeds "
f"MAX_IMPLIED_VERTICES={MAX_IMPLIED_VERTICES}, the cap on a "
"format that describes its points rather than writing them. "
f"file_size_bytes={file_size_bytes} is no evidence either way, so "
"the count is taken on its own. Possible corrupt or malicious file."
)
if declared_n_elems > MAX_SAFE_ELEMENTS:
raise ValidationError(
f"declared_n_elems={declared_n_elems} exceeds MAX_SAFE_ELEMENTS="
f"{MAX_SAFE_ELEMENTS}. Possible corrupt or malicious file."
)
if declared_conn_size > MAX_SAFE_CONN:
raise ValidationError(
f"declared_conn_size={declared_conn_size} exceeds MAX_SAFE_CONN="
f"{MAX_SAFE_CONN}. Possible corrupt or malicious file."
)
if compressed:
return
# coords require 3 * 8 bytes per vertex; allow 4× slack for headers/ASCII overhead
if spells_vertices and declared_n_verts * 3 * 8 > file_size_bytes * 4:
raise ValidationError(
f"declared_n_verts={declared_n_verts} implies "
f"{declared_n_verts * 24} bytes of vertex data but "
f"file_size_bytes={file_size_bytes}. Possible corrupt file."
)
# connectivity requires 4 bytes per index; allow 4× slack
if spells_connectivity and declared_conn_size * 4 > file_size_bytes * 4:
raise ValidationError(
f"declared_conn_size={declared_conn_size} implies "
f"{declared_conn_size * 4} bytes of index data but "
f"file_size_bytes={file_size_bytes}. Possible corrupt file."
)