///|
pub(all) struct IndexEntry {
offset : Int
content_length : Int
} derive(Debug, Eq)
///|
/// Decode SHX entries, checking contiguous byte boundaries from offset 100.
pub fn read_index(
data : Bytes,
limits? : ReadLimits = ReadLimits::default(),
) -> Array[IndexEntry] raise ShapeError {
limits.validate()
if data.length() > limits.max_file_bytes {
raise InvalidData(0, "index exceeds configured byte limit")
}
ignore(read_header(data))
if (data.length() - 100) % 8 != 0 {
raise InvalidData(100, "SHX entry region must be a multiple of eight bytes")
}
let count = (data.length() - 100) / 8
if count > limits.max_records {
raise InvalidData(100, "index exceeds record limit")
}
let r : ByteReader = { data, pos: 100, end: data.length(), base: 0 }
let result : Array[IndexEntry] = []
let mut expected = 100
for _ in 0.. 2147483647 - 8 - offset {
raise InvalidData(r.pos - 8, "SHX end offset overflows")
}
result.push({ offset, content_length })
expected = offset + 8 + content_length
}
result
}
///|
/// Fully verifies the SHX table against decoded SHP records.
pub fn validate_shx(
shp : Bytes,
shx : Bytes,
limits? : ReadLimits = ReadLimits::default(),
) -> Unit raise ShapeError {
let reader = IndexedReader::open(shp, shx, limits~)
for i in 0.. IndexedReader raise ShapeError {
limits.validate()
if shp.length() > limits.max_file_bytes {
raise InvalidData(0, "file exceeds configured byte limit")
}
let file_header = read_header(shp)
let index_header = read_header(shx)
if file_header.kind != index_header.kind ||
file_header.bounds != index_header.bounds ||
file_header.zmin != index_header.zmin ||
file_header.zmax != index_header.zmax ||
file_header.mmin != index_header.mmin ||
file_header.mmax != index_header.mmax {
raise InvalidData(32, "SHP and SHX metadata disagree")
}
let entries = read_index(shx, limits~)
let mut end = 100
for i in 0.. shp.length() - 8 ||
entry.content_length > shp.length() - entry.offset - 8 {
raise InvalidData(entry.offset, "index entry exceeds SHP bytes")
}
let r : ByteReader = {
data: shp,
pos: entry.offset,
end: shp.length(),
base: 0,
}
if r.be32() != i + 1 ||
words_to_bytes(r.be32(), entry.offset + 4) != entry.content_length {
raise InvalidData(entry.offset, "index and record header disagree")
}
end = entry.offset + 8 + entry.content_length
}
if end != shp.length() {
raise InvalidData(end, "index does not account for all SHP bytes")
}
{ data: shp, file_header, entries, limits }
}
///|
pub fn IndexedReader::record_count(self : IndexedReader) -> Int {
self.entries.length()
}
///|
pub fn IndexedReader::header(self : IndexedReader) -> Header {
self.file_header
}
///|
pub fn IndexedReader::entry(
self : IndexedReader,
index : Int,
) -> IndexEntry raise ShapeError {
if index < 0 || index >= self.entries.length() {
raise InvalidData(index, "record index outside table")
}
self.entries[index]
}
///|
pub fn IndexedReader::record_at(
self : IndexedReader,
index : Int,
) -> Record raise ShapeError {
let entry = self.entry(index)
let start = entry.offset + 8
let shape = decode_shape(
self.data[start:start + entry.content_length].to_owned(),
start,
self.limits,
)
if shape.kind != Null && shape.kind != self.file_header.kind {
raise InvalidData(start, "record type differs from file header")
}
match shape.bounds() {
Some(b) =>
if b.xmin < self.file_header.bounds.xmin ||
b.ymin < self.file_header.bounds.ymin ||
b.xmax > self.file_header.bounds.xmax ||
b.ymax > self.file_header.bounds.ymax {
raise InvalidData(start, "record exceeds file bounds")
}
None => ()
}
if shape.kind.has_z() {
for p in shape.points {
match p.z {
Some(z) =>
if z < self.file_header.zmin || z > self.file_header.zmax {
raise InvalidData(start, "Z outside file bounds")
}
None => ()
}
}
}
{
number: index + 1,
offset: entry.offset,
content_length: entry.content_length,
shape,
}
}
///|
/// Only selected records are decoded; order and duplicate indices are preserved.
pub fn IndexedReader::select(
self : IndexedReader,
indices : Array[Int],
) -> Array[Record] raise ShapeError {
let result : Array[Record] = []
for i in indices {
result.push(self.record_at(i))
}
result
}
///|
/// Rebuild a missing index from a validated SHP stream.
pub fn rebuild_index(
shp : Bytes,
limits? : ReadLimits = ReadLimits::default(),
) -> Bytes raise ShapeError {
let reader = ShpReader::open(shp, limits~)
let entries = ByteWriter::new()
while true {
match reader.next() {
None => break
Some(record) => {
entries.be32(record.offset / 2)
entries.be32(record.content_length / 2)
}
}
}
let data = entries.bytes()
let h = reader.header()
let w = ByteWriter::new()
w.append(
write_header(
100 + data.length(),
h.kind,
Some(h.bounds),
Some((h.zmin, h.zmax)),
Some((h.mmin, h.mmax)),
),
)
w.append(data)
w.bytes()
}