Files
pyxis/tools/maps/build_map_pack.py
T
Torlando 9591d8074c fix: make install-marker renewal ownership-checked
Greptile round 9 remediation:

The round-8 heartbeat rewrote the marker unconditionally. If
installer A stalled on a single tile operation for longer than the
marker TTL, installer B could legitimately reclaim the expired
marker, and A's next heartbeat then overwrote B's live claim --
stealing the card out from under a running install.

The renewal now reads the marker before rewriting and only
proceeds when it still carries OUR owner (the same owner comparison
used by release and commit-time verification). A reclaimed,
deleted, or corrupt marker means the claim is void: the install
aborts with a clear "wait for the other installer to finish and
retry" error, leaving the foreign claim intact. The partially
published pack stays device-harmless (no record names it) and the
user retries once the other installer finishes.

- CLI: _renew_install_marker() checks owner before rewriting;
  PackError aborts the install (the existing cleanup paths remove
  the staged temp pack and release the marker as a no-op).
- Flasher: renewInstallMarker() checks owner before rewriting;
  fail() aborts the install (the existing cleanup paths remove the
  owned pack and release the marker as a no-op).
- Tests: CLI 88 (renewal refuses to steal a reclaimed/missing/
  corrupt claim; same-owner renewal still advances the epoch);
  flasher 26 (same contract).
2026-08-29 19:17:08 +00:00

1933 lines
91 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2026 Pyxis contributors
# SPDX-License-Identifier: MIT
"""Build a Pyxis offline map pack exclusively from a local XYZ PNG tree.
Security-sensitive filesystem operations intentionally support Linux only. PNG
support is intentionally limited to non-interlaced images.
Coverage models:
- default: PMPK v1 rectangle extents (contiguous rectangles, optional
antimeridian splits)
- --sparse: PMPK v2 row spans for sparse state/polygon coverage, falling
back to PMPK v3 indexless records when the coverage exceeds the 512-span
cap
- --style: always emit an indexless PMPK v3 pack using the firmware's
canonical style policy metadata; --activate additionally publishes the
v3 active map-set record so the device renders the pack immediately
"""
from __future__ import annotations
import argparse
import contextlib
import ctypes
from dataclasses import dataclass
import errno
import fcntl
import json
import os
from pathlib import Path
import re
import stat
import struct
import sys
import time
from typing import Any, cast, Iterable
import zlib
MAGIC = b"PMPK"
FORMAT_VERSION = 1
SPARSE_FORMAT_VERSION = 2
INDEXLESS_FORMAT_VERSION = 3
SELECTION_MAGIC = b"PMAS"
HEADER_SIZE = 16
EXTENT_SIZE = 26
ROW_SPAN_SIZE = 13
MAX_ROW_SPANS = 512
MAX_ZOOM = 22
MAX_ACTIVE_PACKS = 8 # firmware ActiveMapSetCodec::MAX_PACKS
DEFAULT_MAX_TILES = 100_000
DEFAULT_MAX_BYTES = 8 * 1024 * 1024 * 1024
MAX_VISITED_ENTRIES_BASE = 64
PACK_ID_RE = re.compile(r"[a-z0-9_-]{1,31}\Z")
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
_STRING_LIMITS = {"pack_id": 31, "name": 63, "attribution": 127, "source": 127, "license": 63,
"map_set_id": 31}
MAX_MANIFEST_BYTES = 7100
_DIRECTORY_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
_FILE_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC
if sys.platform.startswith("linux"):
_libc = ctypes.CDLL(None, use_errno=True)
_renameat2 = getattr(_libc, "renameat2", None)
if _renameat2 is not None:
_renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p,
ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
_renameat2.restype = ctypes.c_int
else:
_renameat2 = None
class PackError(ValueError):
"""Input cannot be represented as a safe, complete Pyxis map pack."""
class PublishedDurabilityError(PackError):
"""Publication committed, but syncing its parent directory failed."""
published = True
def __init__(self, target: Path, cause: OSError):
self.target = target
super().__init__(f"pack published at {target}, but durability is uncertain: {cause}")
@dataclass(frozen=True)
class ZoomExtent:
zoom: int
intervals: tuple[tuple[int, int], ...]
y_minimum: int
y_maximum: int
@dataclass(frozen=True)
class RowSpan:
zoom: int
y: int
x_minimum: int
x_maximum: int
@dataclass(frozen=True)
class Tile:
zoom: int
x: int
y: int
path: Path
size: int
zoom_identity: tuple[int, ...] = ()
x_identity: tuple[int, ...] = ()
file_identity: tuple[int, ...] = ()
def _identity(info: os.stat_result) -> tuple[int, ...]:
return (info.st_dev, info.st_ino, info.st_mode, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
def _checked_text(field: str, value: str) -> bytes:
if not value:
raise PackError(f"{field} metadata is required")
try:
encoded = value.encode("ascii")
except UnicodeEncodeError as exc:
raise PackError(f"{field} must contain printable ASCII") from exc
if len(encoded) > _STRING_LIMITS[field] or any(byte < 0x20 or byte > 0x7E for byte in encoded):
raise PackError(f"{field} must be 1-{_STRING_LIMITS[field]} printable ASCII bytes")
return encoded
def _validate_metadata(pack_id: str, name: str, attribution: str, source: str, license: str) -> None:
if not PACK_ID_RE.fullmatch(pack_id):
raise PackError("pack ID must match [a-z0-9_-]{1,31}")
for field, value in (("name", name), ("attribution", attribution), ("source", source), ("license", license)):
_checked_text(field, value)
def _canonical_number(name: str, label: str) -> int:
if not name.isascii() or not name.isdecimal() or str(int(name)) != name:
raise PackError(f"noncanonical {label} path component: {name!r}")
return int(name)
def _fsync_directory_fd(descriptor: int) -> None:
os.fsync(descriptor)
def _open_path(directory: Path, *, create: bool = False) -> int:
path = Path(directory)
if path.is_absolute():
descriptor = os.open("/", _DIRECTORY_FLAGS)
parts = path.parts[1:]
else:
descriptor = os.open(".", _DIRECTORY_FLAGS)
parts = path.parts
try:
for part in parts:
if part in ("", "."):
continue
if part == "..":
raise PackError(f"parent traversal is not allowed: {path}")
try:
child = os.open(part, _DIRECTORY_FLAGS, dir_fd=descriptor)
except FileNotFoundError:
if not create:
raise PackError(f"directory does not exist: {path}")
os.mkdir(part, 0o755, dir_fd=descriptor)
_fsync_directory_fd(descriptor)
child = os.open(part, _DIRECTORY_FLAGS, dir_fd=descriptor)
except OSError as exc:
raise PackError(f"directory must be a real, nonsymlink directory: {path}: {exc}") from exc
os.close(descriptor)
descriptor = child
return descriptor
except BaseException:
os.close(descriptor)
raise
def _mkdir_open(parent_fd: int, name: str) -> tuple[int, tuple[int, ...]]:
try:
os.mkdir(name, 0o755, dir_fd=parent_fd)
_fsync_directory_fd(parent_fd)
except FileExistsError:
pass
try:
descriptor = os.open(name, _DIRECTORY_FLAGS, dir_fd=parent_fd)
except OSError as exc:
raise PackError(f"output component is not a real directory: {name}: {exc}") from exc
return descriptor, _identity(os.fstat(descriptor))
def _read_exact(descriptor: int, size: int, label: str) -> bytes:
result = bytearray()
while len(result) < size:
block = os.read(descriptor, size - len(result))
if not block:
raise PackError(f"invalid PNG structure: {label}")
result.extend(block)
return bytes(result)
def _write_all(descriptor: int, data: bytes | memoryview, label: str) -> None:
remaining = memoryview(data)
while remaining:
written = os.write(descriptor, remaining)
if written <= 0:
raise OSError(errno.EIO, f"short write while writing {label}")
remaining = remaining[written:]
def _inflate_block(inflater: Any, block: bytes, decoded: bytearray,
expected: int, label: str) -> None:
try:
output = inflater.decompress(block, expected + 1 - len(decoded))
except zlib.error as exc:
raise PackError(f"invalid PNG deflate stream: {label}") from exc
decoded.extend(output)
if len(decoded) > expected or inflater.unconsumed_tail or inflater.unused_data:
raise PackError(f"invalid PNG deflate length: {label}")
def _unfilter_png_rows(decoded: bytes, row_bytes: int, bytes_per_pixel: int,
label: str) -> bytes:
stride = row_bytes + 1
rows = len(decoded) // stride
# Fast path: every row uses the None filter (the common case for
# rendered tile PNGs). A single bytes scan is orders of magnitude
# cheaper than the per-pixel predictor loop below.
filter_bytes = bytes(decoded[offset] for offset in range(0, rows * stride, stride))
if filter_bytes == b"\x00" * rows:
output = bytearray()
for offset in range(0, rows * stride, stride):
output.extend(decoded[offset + 1:offset + stride])
return bytes(output)
output = bytearray()
previous = bytearray(row_bytes)
for offset in range(0, len(decoded), stride):
filter_type = decoded[offset]
if filter_type > 4:
raise PackError(f"invalid PNG row filter: {label}")
encoded = decoded[offset + 1:offset + stride]
row = bytearray(row_bytes)
for index, value in enumerate(encoded):
left = row[index - bytes_per_pixel] if index >= bytes_per_pixel else 0
above = previous[index]
upper_left = previous[index - bytes_per_pixel] if index >= bytes_per_pixel else 0
if filter_type == 0:
predictor = 0
elif filter_type == 1:
predictor = left
elif filter_type == 2:
predictor = above
elif filter_type == 3:
predictor = (left + above) // 2
else:
estimate = left + above - upper_left
distances = (abs(estimate - left), abs(estimate - above), abs(estimate - upper_left))
predictor = (left, above, upper_left)[distances.index(min(distances))]
row[index] = (value + predictor) & 0xff
output.extend(row)
previous = row
return bytes(output)
def _validate_png_fd(descriptor: int, label: str, maximum_size: int) -> int:
before = os.fstat(descriptor)
if not stat.S_ISREG(before.st_mode):
raise PackError(f"PNG must be a regular file: {label}")
if before.st_size > maximum_size:
raise PackError(f"total bytes exceed quota of {maximum_size}: {label}")
os.lseek(descriptor, 0, os.SEEK_SET)
if _read_exact(descriptor, 8, label) != PNG_SIGNATURE:
raise PackError(f"invalid PNG signature: {label}")
remaining = before.st_size - 8
first = True
saw_plte = False
saw_idat = False
idat_closed = False
saw_iend = False
inflater = zlib.decompressobj()
decoded = bytearray()
expected = row_bytes = color_type = bit_depth = bytes_per_pixel = 0
palette_entries = 0
while remaining:
header = _read_exact(descriptor, 8, label)
remaining -= 8
length, kind = struct.unpack(">I4s", header)
if length > remaining - 4:
raise PackError(f"invalid PNG structure: {label}")
if first and (kind != b"IHDR" or length != 13):
raise PackError(f"invalid PNG IHDR: {label}")
if any(not (65 <= byte <= 90 or 97 <= byte <= 122) for byte in kind) or kind[2] & 0x20:
raise PackError(f"invalid PNG chunk type: {label}")
if kind == b"PLTE" and length > 768:
raise PackError(f"invalid PNG PLTE: {label}")
if not first and kind == b"IHDR":
raise PackError(f"invalid PNG critical chunk state: {label}")
if kind not in (b"IHDR", b"PLTE", b"IDAT", b"IEND") and kind[0] & 0x20 == 0:
raise PackError(f"unknown PNG critical chunk: {label}")
if saw_idat and kind != b"IDAT" and kind != b"IEND":
idat_closed = True
if kind == b"IDAT" and idat_closed:
raise PackError(f"nonconsecutive PNG IDAT chunks: {label}")
crc = zlib.crc32(kind)
payload = bytearray() if kind in (b"IHDR", b"PLTE") else None
unread = length
while unread:
block = os.read(descriptor, min(unread, 1024 * 1024))
if not block:
raise PackError(f"invalid PNG structure: {label}")
unread -= len(block)
crc = zlib.crc32(block, crc)
if payload is not None:
payload.extend(block)
if kind == b"IDAT":
_inflate_block(inflater, block, decoded, expected, label)
encoded_crc = _read_exact(descriptor, 4, label)
remaining -= length + 4
if crc != struct.unpack(">I", encoded_crc)[0]:
raise PackError(f"invalid PNG chunk CRC: {label}")
if kind == b"IHDR":
assert payload is not None
width, height, bit_depth, color_type, compression, filtering, interlace = struct.unpack(">IIBBBBB", payload)
valid_depths = {0: {1, 2, 4, 8, 16}, 2: {8, 16}, 3: {1, 2, 4, 8}, 4: {8, 16}, 6: {8, 16}}
if (width, height) != (256, 256):
raise PackError(f"PNG must be 256x256: {label}")
if (color_type not in valid_depths or bit_depth not in valid_depths[color_type]
or compression != 0 or filtering != 0 or interlace != 0):
raise PackError(f"invalid or unsupported non-interlaced PNG IHDR: {label}")
channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type]
row_bytes = (width * channels * bit_depth + 7) // 8
bytes_per_pixel = max(1, (channels * bit_depth + 7) // 8)
expected = (row_bytes + 1) * height
first = False
elif kind == b"PLTE":
if saw_plte or saw_idat or color_type in (0, 4) or payload is None:
raise PackError(f"invalid PNG PLTE state: {label}")
entries = len(payload) // 3
if len(payload) == 0 or len(payload) % 3 or entries > 256 or (color_type == 3 and entries > (1 << bit_depth)):
raise PackError(f"invalid PNG PLTE: {label}")
saw_plte = True
palette_entries = entries
elif kind == b"IDAT":
if color_type == 3 and not saw_plte:
raise PackError(f"indexed PNG requires PLTE before IDAT: {label}")
saw_idat = True
elif kind == b"IEND":
if length != 0 or not saw_idat or remaining != 0:
raise PackError(f"invalid PNG IEND: {label}")
saw_iend = True
if first or not saw_iend or not inflater.eof or inflater.unused_data or len(decoded) != expected:
raise PackError(f"invalid PNG structure, deflate, or terminal state: {label}")
pixels = _unfilter_png_rows(bytes(decoded), row_bytes, bytes_per_pixel, label)
if color_type == 3:
if bit_depth == 8:
# Fast path: one C-level scan replaces the 65,536-iteration
# per-pixel loop. For 8-bit index PNGs the packed bytes ARE the
# indices, so any byte at or above the palette length is invalid.
# Single-pass scan via a lookup table: mark every out-of-range
# index byte, then reject if any appear.
bad = bytearray(256)
for index in range(palette_entries, 256):
bad[index] = 1
if bytes.translate(pixels, bytes(bad)).count(1) > 0:
raise PackError(f"indexed PNG palette index out of range: {label}")
else:
for row in range(256):
packed = pixels[row * row_bytes:(row + 1) * row_bytes]
for column in range(256):
bit_offset = column * bit_depth
byte = packed[bit_offset // 8]
shift = 8 - bit_depth - (bit_offset % 8)
if ((byte >> shift) & ((1 << bit_depth) - 1)) >= palette_entries:
raise PackError(f"indexed PNG palette index out of range: {label}")
after = os.fstat(descriptor)
if _identity(before) != _identity(after):
raise PackError(f"PNG changed while being validated: {label}")
return before.st_size
def _open_child_directory(parent_fd: int, name: str, label: str) -> tuple[int, tuple[int, ...]]:
try:
descriptor = os.open(name, _DIRECTORY_FLAGS, dir_fd=parent_fd)
except OSError as exc:
raise PackError(f"symlink or invalid source directory: {label}: {exc}") from exc
return descriptor, _identity(os.fstat(descriptor))
def _discover_tiles_fd(source_fd: int, source_label: Path, max_tiles: int, max_bytes: int) -> list[Tile]:
if max_tiles < 0 or max_bytes < 0:
raise PackError("quotas must not be negative")
if max_tiles > DEFAULT_MAX_TILES or max_bytes > DEFAULT_MAX_BYTES:
raise PackError("quota exceeds importer hard limit")
tiles: list[Tile] = []
keys: set[tuple[int, int, int]] = set()
total_bytes = 0
visited = 0
visit_limit = MAX_VISITED_ENTRIES_BASE + max_tiles * 3
def visit() -> None:
nonlocal visited
visited += 1
if visited > visit_limit:
raise PackError(f"visited-entry quota exceeded ({visit_limit})")
try:
zoom_names = (entry.name for entry in os.scandir(source_fd))
for zoom_name in zoom_names:
visit()
if zoom_name == "metadata.json":
# Optional tile-provider metadata (e.g. Oxed's
# metadata.json). The manifest carries its own style
# metadata, so this file is validated as readable JSON and
# otherwise ignored.
try:
metadata_fd = os.open(zoom_name, _FILE_FLAGS, dir_fd=source_fd)
except OSError as exc:
raise PackError(f"metadata.json is not a regular file: {exc}") from exc
try:
metadata_info = os.fstat(metadata_fd)
if not stat.S_ISREG(metadata_info.st_mode) or metadata_info.st_size > 65536:
raise PackError("metadata.json is oversized")
try:
json.loads(os.read(metadata_fd, metadata_info.st_size).decode("utf-8"))
except (UnicodeDecodeError, ValueError) as exc:
raise PackError(f"metadata.json is not valid JSON: {exc}") from exc
finally:
os.close(metadata_fd)
continue
zoom = _canonical_number(zoom_name, "zoom")
if zoom > MAX_ZOOM:
raise PackError(f"zoom out of range: {zoom}")
zoom_fd, zoom_identity = _open_child_directory(source_fd, zoom_name, f"{source_label}/{zoom_name}")
try:
world_size = 1 << zoom
for x_entry in os.scandir(zoom_fd):
visit()
x = _canonical_number(x_entry.name, "x")
if x >= world_size:
raise PackError(f"x out of range at zoom {zoom}: {x}")
x_fd, x_identity = _open_child_directory(zoom_fd, x_entry.name, f"{source_label}/{zoom}/{x}")
try:
for y_entry in os.scandir(x_fd):
visit()
if not y_entry.name.endswith(".png"):
raise PackError(f"unexpected source entry: {source_label}/{zoom}/{x}/{y_entry.name}")
y = _canonical_number(y_entry.name[:-4], "y")
if y >= world_size:
raise PackError(f"y out of range at zoom {zoom}: {y}")
if len(tiles) >= max_tiles:
raise PackError(f"tile count exceeds quota of {max_tiles}")
key = (zoom, x, y)
if key in keys:
raise PackError(f"duplicate tile key: {zoom}/{x}/{y}")
try:
file_fd = os.open(y_entry.name, _FILE_FLAGS, dir_fd=x_fd)
except OSError as exc:
raise PackError(f"symlink or invalid PNG: {source_label}/{zoom}/{x}/{y}.png: {exc}") from exc
try:
info = os.fstat(file_fd)
if not stat.S_ISREG(info.st_mode):
raise PackError(f"PNG must be a regular file: {source_label}/{zoom}/{x}/{y}.png")
remaining_bytes = max_bytes - total_bytes
if info.st_size > remaining_bytes:
raise PackError(f"total bytes exceed quota of {max_bytes}")
size = _validate_png_fd(file_fd, f"{source_label}/{zoom}/{x}/{y}.png", remaining_bytes)
identity = _identity(os.fstat(file_fd))
finally:
os.close(file_fd)
keys.add(key)
total_bytes += size
tiles.append(Tile(zoom, x, y, source_label / str(zoom) / str(x) / f"{y}.png",
size, zoom_identity, x_identity, identity))
finally:
os.close(x_fd)
finally:
os.close(zoom_fd)
except OSError as exc:
raise PackError(f"cannot read source directory {source_label}: {exc}") from exc
if not tiles:
raise PackError("source contains no tiles")
return sorted(tiles, key=lambda tile: (tile.zoom, tile.x, tile.y))
def discover_tiles(source_directory: Path, max_tiles: int, max_bytes: int) -> list[Tile]:
descriptor = _open_path(Path(source_directory))
try:
return _discover_tiles_fd(descriptor, Path(source_directory), max_tiles, max_bytes)
finally:
os.close(descriptor)
def _runs(values: set[int]) -> tuple[tuple[int, int], ...]:
ordered = sorted(values)
runs: list[tuple[int, int]] = []
start = previous = ordered[0]
for value in ordered[1:]:
if value != previous + 1:
runs.append((start, previous))
start = value
previous = value
runs.append((start, previous))
return tuple(runs)
def calculate_extents(tiles: Iterable[Tile], antimeridian_zooms: set[int]) -> list[ZoomExtent]:
by_zoom: dict[int, set[tuple[int, int]]] = {}
for tile in tiles:
by_zoom.setdefault(tile.zoom, set()).add((tile.x, tile.y))
zooms = sorted(by_zoom)
if zooms != list(range(zooms[0], zooms[-1] + 1)):
raise PackError("zoom levels must be contiguous")
if not antimeridian_zooms.issubset(by_zoom):
raise PackError("antimeridian zoom option names a missing zoom")
extents: list[ZoomExtent] = []
for zoom in zooms:
keys = by_zoom[zoom]
xs = {key[0] for key in keys}
ys = {key[1] for key in keys}
x_runs = _runs(xs)
y_runs = _runs(ys)
if len(y_runs) != 1:
raise PackError(f"incomplete rectangle at zoom {zoom}: noncontiguous y range")
if zoom in antimeridian_zooms:
world_maximum = (1 << zoom) - 1
if len(x_runs) != 2 or x_runs[0][0] != 0 or x_runs[1][1] != world_maximum:
raise PackError(f"ambiguous antimeridian split at zoom {zoom}")
intervals = x_runs
else:
if len(x_runs) != 1:
raise PackError(f"incomplete rectangle at zoom {zoom}: noncontiguous x range")
intervals = x_runs
expected = sum(last - first + 1 for first, last in intervals) * (y_runs[0][1] - y_runs[0][0] + 1)
if len(keys) != expected:
raise PackError(f"incomplete rectangle at zoom {zoom}: missing tile")
extents.append(ZoomExtent(zoom, intervals, y_runs[0][0], y_runs[0][1]))
return extents
def calculate_row_spans(tiles: Iterable[Tile]) -> list[RowSpan]:
by_row: dict[tuple[int, int], set[int]] = {}
zooms: set[int] = set()
for tile in tiles:
by_row.setdefault((tile.zoom, tile.y), set()).add(tile.x)
zooms.add(tile.zoom)
ordered_zooms = sorted(zooms)
if not ordered_zooms or ordered_zooms != list(range(ordered_zooms[0], ordered_zooms[-1] + 1)):
raise PackError("zoom levels must be contiguous")
spans: list[RowSpan] = []
for (zoom, y), xs in sorted(by_row.items()):
for minimum, maximum in _runs(xs):
spans.append(RowSpan(zoom, y, minimum, maximum))
if len(spans) > MAX_ROW_SPANS:
raise PackError(f"sparse coverage exceeds row-span limit of {MAX_ROW_SPANS}")
return spans
def _validate_row_spans(row_spans: list[RowSpan], tile_count: int) -> None:
if not row_spans or len(row_spans) > MAX_ROW_SPANS:
raise PackError("manifest row-span count is invalid")
zooms: set[int] = set()
total = 0
previous: RowSpan | None = None
for span in row_spans:
if span.zoom < 0 or span.zoom > MAX_ZOOM:
raise PackError("invalid manifest row span")
world_maximum = (1 << span.zoom) - 1
if span.y < 0 or span.y > world_maximum or span.x_minimum < 0 \
or span.x_minimum > span.x_maximum or span.x_maximum > world_maximum:
raise PackError("invalid manifest row span")
if previous is not None and ((span.zoom, span.y) < (previous.zoom, previous.y) or
((span.zoom, span.y) == (previous.zoom, previous.y) and
span.x_minimum <= previous.x_maximum + 1)):
raise PackError("manifest row spans are not canonical")
total += span.x_maximum - span.x_minimum + 1
if total > 0xFFFFFFFF:
raise PackError("manifest tile count overflows uint32")
zooms.add(span.zoom)
previous = span
ordered_zooms = sorted(zooms)
if ordered_zooms != list(range(ordered_zooms[0], ordered_zooms[-1] + 1)):
raise PackError("manifest row spans must cover contiguous zooms")
if tile_count <= 0 or tile_count != total:
raise PackError("invalid manifest tile count")
def serialize_sparse_manifest(*, pack_id: str, name: str, attribution: str, source: str,
license: str, row_spans: list[RowSpan], tile_count: int) -> bytes:
_validate_metadata(pack_id, name, attribution, source, license)
_validate_row_spans(row_spans, tile_count)
payload = bytearray()
for field, value in (("pack_id", pack_id), ("name", name), ("attribution", attribution),
("source", source), ("license", license)):
encoded = _checked_text(field, value)
payload.append(len(encoded))
payload.extend(encoded)
payload.extend((row_spans[0].zoom, row_spans[-1].zoom))
payload.extend(struct.pack("<HI", len(row_spans), tile_count))
for span in row_spans:
payload.extend(struct.pack("<BIII", span.zoom, span.y, span.x_minimum, span.x_maximum))
length = HEADER_SIZE + len(payload) + 4
result = bytearray(struct.pack("<4sBBHII", MAGIC, SPARSE_FORMAT_VERSION, 0, HEADER_SIZE, length, 0))
result.extend(payload)
result.extend(struct.pack("<I", zlib.crc32(result)))
return bytes(result)
def serialize_indexless_manifest(*, pack_id: str, name: str, attribution: str, source: str,
license: str, min_zoom: int, max_zoom: int,
tile_count: int) -> bytes:
_validate_metadata(pack_id, name, attribution, source, license)
if min_zoom < 0 or min_zoom > max_zoom or max_zoom > MAX_ZOOM or tile_count <= 0:
raise PackError("indexless manifest range or tile count is invalid")
payload = bytearray()
for field, value in (("pack_id", pack_id), ("name", name), ("attribution", attribution),
("source", source), ("license", license)):
encoded = _checked_text(field, value)
payload.append(len(encoded))
payload.extend(encoded)
payload.extend((min_zoom, max_zoom))
payload.extend(struct.pack("<I", tile_count))
length = HEADER_SIZE + len(payload) + 4
result = bytearray(struct.pack("<4sBBHII", MAGIC, INDEXLESS_FORMAT_VERSION, 0, HEADER_SIZE, length, 0))
result.extend(payload)
result.extend(struct.pack("<I", zlib.crc32(result)))
return bytes(result)
def _validate_manifest_values(extents: list[ZoomExtent], tile_count: int) -> None:
if not extents or len(extents) > MAX_ZOOM + 1:
raise PackError("manifest extent count is invalid")
if extents[0].zoom < 0 or extents[-1].zoom > MAX_ZOOM or [item.zoom for item in extents] != list(
range(extents[0].zoom, extents[-1].zoom + 1)):
raise PackError("manifest extents must cover contiguous valid zooms")
total = 0
for extent in extents:
world_maximum = (1 << extent.zoom) - 1
if len(extent.intervals) not in (1, 2) or extent.y_minimum < 0 or extent.y_minimum > extent.y_maximum \
or extent.y_maximum > world_maximum:
raise PackError("invalid manifest extent")
columns = 0
for minimum, maximum in extent.intervals:
if minimum < 0 or minimum > maximum or maximum > world_maximum:
raise PackError("invalid manifest extent")
columns += maximum - minimum + 1
if len(extent.intervals) == 2:
first, second = extent.intervals
if first[0] != 0 or second[1] != world_maximum or first[1] >= second[0] or first[1] + 1 == second[0]:
raise PackError("invalid manifest antimeridian intervals")
level = columns * (extent.y_maximum - extent.y_minimum + 1)
if level > 0xFFFFFFFF or total > 0xFFFFFFFF - level:
raise PackError("manifest tile count overflows uint32")
total += level
if tile_count <= 0 or tile_count > 0xFFFFFFFF or total != tile_count:
raise PackError("invalid manifest tile count")
def serialize_manifest(*, pack_id: str, name: str, attribution: str, source: str,
license: str, extents: list[ZoomExtent], tile_count: int) -> bytes:
_validate_metadata(pack_id, name, attribution, source, license)
_validate_manifest_values(extents, tile_count)
payload = bytearray()
for field, value in (("pack_id", pack_id), ("name", name), ("attribution", attribution),
("source", source), ("license", license)):
encoded = _checked_text(field, value)
payload.append(len(encoded))
payload.extend(encoded)
payload.extend((extents[0].zoom, extents[-1].zoom, len(extents)))
payload.extend(struct.pack("<I", tile_count))
for extent in extents:
intervals = extent.intervals + (((0, 0),) if len(extent.intervals) == 1 else ())
payload.extend((extent.zoom, len(extent.intervals)))
payload.extend(struct.pack("<II", extent.y_minimum, extent.y_maximum))
for minimum, maximum in intervals:
payload.extend(struct.pack("<II", minimum, maximum))
length = HEADER_SIZE + len(payload) + 4
result = bytearray(struct.pack("<4sBBHII", MAGIC, FORMAT_VERSION, 0, HEADER_SIZE, length, 0))
result.extend(payload)
result.extend(struct.pack("<I", zlib.crc32(result)))
return bytes(result)
def parse_manifest(data: bytes) -> dict[str, object]:
if len(data) < 20 or len(data) > MAX_MANIFEST_BYTES:
raise PackError("manifest is truncated or oversized")
magic, version, reserved, header_size, length, reserved_word = struct.unpack("<4sBBHII", data[:16])
if magic != MAGIC or version not in (FORMAT_VERSION, SPARSE_FORMAT_VERSION,
INDEXLESS_FORMAT_VERSION) or \
(reserved, header_size, length, reserved_word) != (0, 16, len(data), 0):
raise PackError("invalid manifest header")
if zlib.crc32(data[:-4]) != struct.unpack("<I", data[-4:])[0]:
raise PackError("invalid manifest CRC")
position = 16
values: dict[str, object] = {}
for field in ("pack_id", "name", "attribution", "source", "license"):
if position >= len(data) - 4:
raise PackError("manifest string is truncated")
size = data[position]
position += 1
if size > len(data) - 4 - position:
raise PackError("manifest string is truncated")
try:
value = data[position:position + size].decode("ascii")
except UnicodeDecodeError as exc:
raise PackError("invalid manifest string") from exc
position += size
_checked_text(field, value)
if field == "pack_id" and not PACK_ID_RE.fullmatch(value):
raise PackError("invalid manifest pack ID")
values[field] = value
if version == INDEXLESS_FORMAT_VERSION:
if position + 6 > len(data) - 4:
raise PackError("manifest indexless fields are truncated")
minimum, maximum = data[position:position + 2]
position += 2
tile_count = struct.unpack("<I", data[position:position + 4])[0]
position += 4
if position != len(data) - 4 or minimum > maximum or maximum > MAX_ZOOM or not tile_count:
raise PackError("invalid indexless manifest fields")
values.update(format_version=version, min_zoom=minimum, max_zoom=maximum,
tile_count=tile_count, extents=[], row_spans=[])
return values
if version == FORMAT_VERSION:
if position + 7 > len(data) - 4:
raise PackError("manifest fields are truncated")
minimum, maximum, count = data[position:position + 3]
position += 3
tile_count = struct.unpack("<I", data[position:position + 4])[0]
position += 4
if count > MAX_ZOOM + 1 or count > (len(data) - 4 - position) // EXTENT_SIZE:
raise PackError("invalid manifest extent count")
extents: list[ZoomExtent] = []
for _ in range(count):
zoom, interval_count = data[position:position + 2]
y_minimum, y_maximum, x0, x1, x2, x3 = struct.unpack("<IIIIII", data[position + 2:position + 26])
position += EXTENT_SIZE
if interval_count not in (1, 2) or (interval_count == 1 and (x2 != 0 or x3 != 0)):
raise PackError("invalid manifest extent")
intervals = ((x0, x1),) if interval_count == 1 else ((x0, x1), (x2, x3))
extents.append(ZoomExtent(zoom, intervals, y_minimum, y_maximum))
if position != len(data) - 4 or not extents or minimum != extents[0].zoom or maximum != extents[-1].zoom:
raise PackError("invalid manifest length or zoom range")
_validate_manifest_values(extents, tile_count)
values.update(format_version=version, min_zoom=minimum, max_zoom=maximum,
tile_count=tile_count, extents=extents, row_spans=[])
else:
if position + 8 > len(data) - 4:
raise PackError("manifest sparse fields are truncated")
minimum, maximum = data[position:position + 2]
position += 2
count, tile_count = struct.unpack("<HI", data[position:position + 6])
position += 6
if count == 0 or count > MAX_ROW_SPANS or count > (len(data) - 4 - position) // ROW_SPAN_SIZE:
raise PackError("invalid manifest row-span count")
row_spans: list[RowSpan] = []
for _ in range(count):
zoom, y, x_minimum, x_maximum = struct.unpack("<BIII", data[position:position + ROW_SPAN_SIZE])
position += ROW_SPAN_SIZE
row_spans.append(RowSpan(zoom, y, x_minimum, x_maximum))
if position != len(data) - 4 or minimum != row_spans[0].zoom or maximum != row_spans[-1].zoom:
raise PackError("invalid manifest length or zoom range")
_validate_row_spans(row_spans, tile_count)
values.update(format_version=version, min_zoom=minimum, max_zoom=maximum,
tile_count=tile_count, extents=[], row_spans=row_spans)
return values
def encode_active_map_set(*, generation: int, map_set_id: str, attribution: str,
pack_ids: list[str]) -> bytes:
"""Serialize a v3 (indexless) PMAS active map-set record.
Byte-compatible with the web flasher's encodeActiveMapSet and the
firmware's ActiveMapSetCodec (PMAS magic, version 3, u16 total length,
u32 generation, then length-prefixed map-set ID, attribution, pack
count, and ordered pack IDs, trailing CRC-32).
"""
if not 1 <= generation <= 0xFFFFFFFF:
raise PackError("active map-set generation is invalid")
map_set = _checked_text("map_set_id", map_set_id)
credit = _checked_text("attribution", attribution)
if not PACK_ID_RE.fullmatch(map_set_id) or not pack_ids or len(pack_ids) > MAX_ACTIVE_PACKS:
raise PackError(f"invalid active map-set packs (limit {MAX_ACTIVE_PACKS})")
seen: set[str] = set()
encoded_packs: list[bytes] = []
for pack_id in pack_ids:
identifier = _checked_text("pack_id", pack_id)
if not PACK_ID_RE.fullmatch(pack_id) or pack_id in seen:
raise PackError("invalid or duplicate active map-set pack")
seen.add(pack_id)
encoded_packs.append(identifier)
length = 12 + 1 + len(map_set) + 1 + len(credit) + 1 + sum(1 + len(item) for item in encoded_packs) + 4
result = bytearray(struct.pack("<4sBBHI", SELECTION_MAGIC, INDEXLESS_FORMAT_VERSION, 0, length, generation))
result.append(len(map_set)); result.extend(map_set)
result.append(len(credit)); result.extend(credit)
result.append(len(encoded_packs))
for identifier in encoded_packs:
result.append(len(identifier)); result.extend(identifier)
result.extend(struct.pack("<I", zlib.crc32(result)))
return bytes(result)
def parse_active_map_set(data: bytes) -> dict[str, object]:
if len(data) < 16 or len(data) > 7105:
raise PackError("active map-set record length is invalid")
magic, version, reserved, length, generation = struct.unpack("<4sBBHI", data[:12])
if magic != SELECTION_MAGIC or version != INDEXLESS_FORMAT_VERSION or reserved != 0 \
or length != len(data) or generation == 0 \
or zlib.crc32(data[:-4]) != struct.unpack("<I", data[-4:])[0]:
raise PackError("invalid active map-set record")
position = 12
def read_text(label: str, maximum: int, identifier: bool) -> str:
nonlocal position
if position >= len(data) - 4:
raise PackError(f"{label} is truncated")
size = data[position]
position += 1
if size == 0 or size > maximum or position + size > len(data) - 4:
raise PackError(f"{label} is invalid")
value = data[position:position + size].decode("ascii")
position += size
if any(byte < 0x20 or byte > 0x7E for byte in value.encode("ascii")) or \
(identifier and not PACK_ID_RE.fullmatch(value)):
raise PackError(f"{label} is invalid")
return value
map_set_id = read_text("map set ID", 31, True)
attribution = read_text("attribution", 127, False)
if position >= len(data) - 4:
raise PackError("active map-set pack count is truncated")
count = data[position]
position += 1
if count == 0 or count > 8:
raise PackError("active map-set pack count is invalid")
packs: list[str] = []
for _ in range(count):
pack_id = read_text("pack ID", 31, True)
if pack_id in packs:
raise PackError("duplicate active map-set pack")
packs.append(pack_id)
if position != len(data) - 4:
raise PackError("active map-set record has trailing data")
return {"format_version": version, "generation": generation, "map_set_id": map_set_id,
"attribution": attribution, "packs": packs}
def _open_verified_tile(source_fd: int, tile: Tile) -> int:
zoom_fd, zoom_identity = _open_child_directory(source_fd, str(tile.zoom), str(tile.path.parent.parent))
try:
if zoom_identity != tile.zoom_identity:
raise PackError(f"source directory changed after validation: {tile.path}")
x_fd, x_identity = _open_child_directory(zoom_fd, str(tile.x), str(tile.path.parent))
try:
if x_identity != tile.x_identity:
raise PackError(f"source directory changed after validation: {tile.path}")
try:
descriptor = os.open(f"{tile.y}.png", _FILE_FLAGS, dir_fd=x_fd)
except OSError as exc:
raise PackError(f"source PNG changed, became a symlink, or is invalid: {tile.path}: {exc}") from exc
if _identity(os.fstat(descriptor)) != tile.file_identity:
os.close(descriptor)
raise PackError(f"source PNG changed after validation: {tile.path}")
return descriptor
finally:
os.close(x_fd)
finally:
os.close(zoom_fd)
def _ensure_stage_path(stage_fd: int, tile: Tile) -> tuple[int, str]:
tiles_fd, _ = _mkdir_open(stage_fd, "tiles")
try:
zoom_fd, _ = _mkdir_open(tiles_fd, str(tile.zoom))
try:
x_fd, _ = _mkdir_open(zoom_fd, str(tile.x))
return x_fd, f"{tile.y}.png"
finally:
os.close(zoom_fd)
finally:
os.close(tiles_fd)
def copy_tile(source_fd: int, tile: Tile, stage_fd: int, maximum_bytes: int) -> None:
input_fd = _open_verified_tile(source_fd, tile)
output_parent_fd = -1
output_fd = -1
try:
output_parent_fd, output_name = _ensure_stage_path(stage_fd, tile)
output_fd = os.open(output_name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC,
0o644, dir_fd=output_parent_fd)
copied = 0
while True:
block = os.read(input_fd, min(1024 * 1024, maximum_bytes + 1 - copied))
if not block:
break
copied += len(block)
if copied > maximum_bytes:
raise PackError(f"total bytes exceed quota of {maximum_bytes}")
_write_all(output_fd, block, f"staged {tile.zoom}/{tile.x}/{tile.y}.png")
if copied != tile.size or _identity(os.fstat(input_fd)) != tile.file_identity:
raise PackError(f"source PNG changed while copying: {tile.path}")
_validate_png_fd(output_fd, f"staged {tile.zoom}/{tile.x}/{tile.y}.png", maximum_bytes)
os.fsync(output_fd)
_fsync_directory_fd(output_parent_fd)
finally:
if output_fd >= 0:
os.close(output_fd)
if output_parent_fd >= 0:
os.close(output_parent_fd)
os.close(input_fd)
def _read_file_at(parent_fd: int, name: str, maximum: int) -> bytes:
descriptor = os.open(name, _FILE_FLAGS, dir_fd=parent_fd)
try:
info = os.fstat(descriptor)
if not stat.S_ISREG(info.st_mode) or info.st_size > maximum:
raise PackError(f"invalid or oversized file: {name}")
return _read_exact(descriptor, info.st_size, name)
finally:
os.close(descriptor)
def _read_selection_at(parent_fd: int, name: str) -> bytes | None:
try:
return _read_file_at(parent_fd, name, 7105)
except FileNotFoundError:
return None
except OSError as exc:
raise PackError(f"cannot read {name}: {exc}") from exc
def decode_active_selection(data: bytes) -> dict[str, object]:
"""Decode v1/v2/v3 PMAS active map-set records.
Byte-compatible with the firmware's ActiveMapSetCodec and the web
flasher's decodeActiveSelection. Row spans in v2 records are skipped
with bound checks only; this tool never emits spans.
"""
if len(data) < 16 or len(data) > 7105:
raise PackError("active map-set record length is invalid")
magic, version, reserved, length, generation = struct.unpack("<4sBBHI", data[:12])
if magic != SELECTION_MAGIC or version not in (1, 2, 3) or reserved != 0 \
or length != len(data) or generation == 0 \
or zlib.crc32(data[:-4]) != struct.unpack("<I", data[-4:])[0]:
raise PackError("invalid active map-set record")
end = len(data) - 4
if version == 1:
if len(data) != 48:
raise PackError("invalid v1 active map-set length")
size = data[12]
if not 1 <= size <= 31:
raise PackError("invalid v1 active map-set pack ID")
pack_id = data[13:13 + size].decode("ascii")
if any(byte != 0 for byte in data[13 + size:end]) or not PACK_ID_RE.fullmatch(pack_id):
raise PackError("invalid v1 active map-set record")
return {"format_version": 1, "generation": generation, "map_set_id": None,
"attribution": None, "packs": [pack_id]}
position = 12
def read_text(label: str, maximum: int, identifier: bool) -> str:
nonlocal position
if position >= end:
raise PackError(f"{label} is truncated")
size = data[position]
position += 1
if size == 0 or size > maximum or position + size > end:
raise PackError(f"{label} is invalid")
value = data[position:position + size].decode("ascii")
position += size
if any(byte < 0x20 or byte > 0x7E for byte in value.encode("ascii")) or \
(identifier and not PACK_ID_RE.fullmatch(value)):
raise PackError(f"{label} is invalid")
return value
map_set_id = read_text("map set ID", 31, True)
attribution = read_text("attribution", 127, False)
if position >= end:
raise PackError("active map-set pack count is truncated")
count = data[position]
position += 1
if count == 0 or count > 8:
raise PackError("active map-set pack count is invalid")
packs: list[str] = []
total_spans = 0
for _ in range(count):
pack_id = read_text("pack ID", 31, True)
if pack_id in packs:
raise PackError("duplicate active map-set pack")
packs.append(pack_id)
if version == 3:
continue
if position + 2 > end:
raise PackError("active map-set span count is truncated")
span_count = struct.unpack("<H", data[position:position + 2])[0]
position += 2
if span_count == 0 or span_count > MAX_ROW_SPANS or total_spans > MAX_ROW_SPANS - span_count \
or position + span_count * ROW_SPAN_SIZE > end:
raise PackError("active map-set row-span count is invalid")
total_spans += span_count
position += span_count * ROW_SPAN_SIZE
if position != end:
raise PackError("active map-set record has trailing data")
return {"format_version": version, "generation": generation, "map_set_id": map_set_id,
"attribution": attribution, "packs": packs}
def _validate_pack_fd(pack_fd: int, label: Path, max_bytes: int) -> dict[str, object]:
parsed = parse_manifest(_read_file_at(pack_fd, "manifest.pmp", MAX_MANIFEST_BYTES))
tiles_fd, _ = _open_child_directory(pack_fd, "tiles", f"{label}/tiles")
try:
tile_count = cast(int, parsed["tile_count"])
tiles = _discover_tiles_fd(tiles_fd, label / "tiles", tile_count, max_bytes)
finally:
os.close(tiles_fd)
if len(tiles) != tile_count:
raise PackError("emitted tile tree does not match manifest")
if parsed["format_version"] == INDEXLESS_FORMAT_VERSION:
zooms = sorted({tile.zoom for tile in tiles})
if (cast(int, parsed["min_zoom"]), cast(int, parsed["max_zoom"])) != (zooms[0], zooms[-1]) \
or zooms != list(range(zooms[0], zooms[-1] + 1)):
raise PackError("emitted tile tree does not match indexless manifest")
return parsed
if parsed["format_version"] == FORMAT_VERSION:
expected_extents = cast(list[ZoomExtent], parsed["extents"])
extents = calculate_extents(tiles, {item.zoom for item in expected_extents if len(item.intervals) == 2})
if extents != expected_extents:
raise PackError("emitted tile tree does not match manifest")
else:
expected_spans = cast(list[RowSpan], parsed["row_spans"])
if calculate_row_spans(tiles) != expected_spans:
raise PackError("emitted tile tree does not match sparse manifest")
return parsed
def validate_pack(pack_directory: Path, max_bytes: int = DEFAULT_MAX_BYTES) -> dict[str, object]:
descriptor = _open_path(Path(pack_directory))
try:
return _validate_pack_fd(descriptor, Path(pack_directory), max_bytes)
finally:
os.close(descriptor)
def _existing_pack_fd(packs_fd: int, pack_id: str) -> int | None:
"""Open a published pack directory if it exists; None otherwise."""
try:
return os.open(pack_id, _DIRECTORY_FLAGS, dir_fd=packs_fd)
except FileNotFoundError:
return None
except OSError as exc:
raise PackError(f"existing pack is not a real directory: {pack_id}: {exc}") from exc
def _verify_existing_pack(source_fd: int, pack_fd: int, pack_id: str,
expected_manifest: bytes, tiles: list[Tile],
max_bytes: int) -> None:
"""Verify a published pack byte-for-byte against the source tiles.
Mirrors the web flasher's verifyExistingPack: a pack that already
exists may only be resumed when its manifest and every tile match the
input exactly, so a resume never republishes different content under
an existing name.
"""
try:
raw_manifest = _read_file_at(pack_fd, "manifest.pmp", MAX_MANIFEST_BYTES)
except FileNotFoundError:
raise PackError(f"existing pack {pack_id} is incomplete: manifest.pmp missing") from None
if raw_manifest != expected_manifest:
raise PackError(f"existing pack {pack_id} does not match this source and "
"cannot be resumed")
tiles_fd, _ = _open_child_directory(pack_fd, "tiles", f"{pack_id}/tiles")
try:
actual = _discover_tiles_fd(tiles_fd, Path(pack_id) / "tiles", len(tiles), max_bytes)
finally:
os.close(tiles_fd)
if len(actual) != len(tiles):
raise PackError(f"existing pack {pack_id} tile count does not match this source")
input_by_key = {(tile.zoom, tile.x, tile.y): tile for tile in tiles}
for actual_tile in actual:
key = (actual_tile.zoom, actual_tile.x, actual_tile.y)
source_tile = input_by_key.get(key)
if source_tile is None:
raise PackError(f"existing pack {pack_id} contains a tile this source does not")
existing_bytes = _read_file_at(pack_fd, f"tiles/{actual_tile.zoom}/"
f"{actual_tile.x}/{actual_tile.y}.png", max_bytes)
input_fd = _open_verified_tile(source_fd, source_tile)
try:
input_bytes = _read_exact(input_fd, source_tile.size, str(source_tile.path))
finally:
os.close(input_fd)
if existing_bytes != input_bytes:
raise PackError(f"existing pack {pack_id} tile {actual_tile.zoom}/"
f"{actual_tile.x}/{actual_tile.y}.png differs from this source")
STYLE_POLICIES: dict[str, dict[str, str]] = {
"osm-bright": {
"attribution": "(c) OpenMapTiles (c) OpenStreetMap contributors",
"source": "Oxed's Map Tile Downloader (OSM Bright)",
"license": "OSM ODbL; style CC-BY-4.0/BSD-3-Clause",
},
"dark-matter": {
"attribution": "(c) OpenMapTiles (c) OpenStreetMap contributors; style (c) CARTO",
"source": "Oxed's Map Tile Downloader (Dark Matter)",
"license": "OSM ODbL; style CC-BY-4.0/BSD-3-Clause (CARTO CC-BY-3.0)",
},
"positron": {
"attribution": "(c) OpenMapTiles (c) OpenStreetMap contributors; style (c) CARTO",
"source": "Oxed's Map Tile Downloader (Positron)",
"license": "OSM ODbL; style CC-BY-4.0/BSD-3-Clause (CARTO CC-BY-3.0)",
},
"toner": {
"attribution": "(c) MapTiler (c) OpenStreetMap contributors",
"source": "Oxed's Map Tile Downloader (Toner)",
"license": "OSM ODbL; style CC-BY-4.0/BSD-3-Clause (Stamen ISC)",
},
}
def activate_map_set(pyxis_fd: int, *, pack_id: str, map_set_id: str, attribution: str,
marker_token: str | None = None) -> tuple[str, list[str]]:
"""Publish the v3 active map-set record, mirroring the web flasher.
pyxis_fd is the open pyxis-map/ directory. Slot and style-record
selection follows the flasher's prepareActiveMapSet/activateMapSet:
read both slots, reject conflicting same-generation records, resolve
the pack list from the installed style record (falling back to the
highest-generation slot of the same style), then publish the record to
the style record under map-sets/ first and the chosen active-pack slot
second. Both writes go through _write_atomic_at (exclusive temp file,
fsync, rename, read-back) so an interrupted activation never truncates
or leaves a partial record: a crash mid-write leaves the previous
record intact and a retry (or the device's own style re-activation)
completes the activation. The style record is written first because the
firmware's device-side style activation (MapStyleCatalog::activate)
resequences the style record's composition into the active slot, so the
style record must never be older than the slot -- otherwise a stale
style record would reseed the slot and drop a newly installed pack.
The final pack list is validated against the firmware's pack limit
before any byte is written, so a rejected activation cannot orphan a
published pack.
marker_token is the caller's cross-producer install-marker token (see
_acquire_install_marker). When given, the raw slot and style-record
bytes are captured when they are first read and re-read immediately
before the record writes: if another installer (the web flasher holds
a Web Locks name, which this process's flock cannot see) committed new
activation state -- or holds a fresh install marker that is not ours --
in the meantime, the publication is aborted before any byte is written.
The already-published pack is kept; a retry re-derives from the new
state and converges, and a pack that no record names is device-harmless
(MapTilePack only reads packs enumerated in the active selection).
Returns (slot_name, enabled_packs).
"""
slot_names = ("active-pack.0", "active-pack.1")
slots: list[dict[str, object] | None] = [None, None]
slot_raw: list[bytes | None] = [None, None]
for index, slot_name in enumerate(slot_names):
raw = _read_selection_at(pyxis_fd, slot_name)
slot_raw[index] = raw
if raw is not None:
slots[index] = decode_active_selection(raw)
if slots[0] is not None and slots[1] is not None \
and slots[0]["generation"] == slots[1]["generation"] and slots[0] != slots[1]:
raise PackError("conflicting active map-set records")
valid = [slot for slot in slots if slot is not None]
highest = max([0] + [cast(int, slot["generation"]) for slot in valid])
if highest == 0xFFFFFFFF:
raise PackError("active selection generation is exhausted")
current = max(valid, key=lambda slot: cast(int, slot["generation"])) if valid else None
map_sets_fd, _ = _mkdir_open(pyxis_fd, "map-sets")
try:
style_name = f"{map_set_id}.pmas"
installed: dict[str, object] | None = None
installed_raw = _read_selection_at(map_sets_fd, style_name)
if installed_raw is not None:
installed = decode_active_selection(installed_raw)
if installed["format_version"] not in (2, 3) \
or installed["map_set_id"] != map_set_id \
or installed["attribution"] != attribution:
raise PackError("installed style record does not match selected map set")
composition: dict[str, object] | None = installed
if composition is None and current is not None \
and current["format_version"] in (2, 3) and current["map_set_id"] == map_set_id:
composition = current
packs: list[str] = []
if composition is not None:
if composition["attribution"] != attribution:
raise PackError("installed map set attribution does not match")
packs = [value for value in cast(list[str], composition["packs"]) if value != pack_id]
packs.insert(0, pack_id)
if len(packs) > MAX_ACTIVE_PACKS:
raise PackError(f"active map-set pack limit exceeded: {len(packs)} > "
f"{MAX_ACTIVE_PACKS}; remove an installed pack before activating")
record = encode_active_map_set(generation=highest + 1, map_set_id=map_set_id,
attribution=attribution, pack_ids=packs)
if slots[0] is None:
target = slot_names[0]
elif slots[1] is None:
target = slot_names[1]
else:
target = slot_names[0] if cast(int, slots[0]["generation"]) <= \
cast(int, slots[1]["generation"]) else slot_names[1]
# Commit-time revalidation: the slot/style records were derived
# above; a concurrent cross-producer installer (the web flasher's
# Web Locks name is invisible to our flock) may have committed new
# activation state in the meantime. Re-reading the raw bytes here
# and aborting on any change guarantees this publication never
# overwrites a newer record with one derived from stale state.
# installed_raw is the style record's bytes as first read above.
_verify_activation_state_at(pyxis_fd, map_sets_fd, slot_raw=slot_raw,
style_raw=installed_raw, style_name=style_name,
marker_token=marker_token)
# Style record first, active slot second (the web flasher's order).
# The firmware's device-side style activation
# (MapStyleCatalog::activate) reads the style record as the source
# of truth and resequences its composition into the active slot.
# If the style record were ever older than the slot, that path
# would reseed the slot from the stale composition and drop a
# newly installed pack. Writing the style record first guarantees
# the style record is never older than the slot, so every recovery
# path (CLI retry, device-side style re-activation) converges
# forward to the newest composition. The worst interruption is
# "slot one build stale, style record current" -- the map still
# serves from the previous pack and the next style cycle or
# builder retry completes the new activation. Both records carry
# the identical PMAS payload.
_write_atomic_at(map_sets_fd, style_name, record)
_write_atomic_at(pyxis_fd, target, record)
finally:
os.close(map_sets_fd)
return target, packs
def _write_atomic_at(parent_fd: int, name: str, data: bytes) -> None:
"""Atomically replace ``name`` with ``data`` using an exclusive temp file.
Writes to a per-process temp name, fsyncs it, then renameat()s it over
the destination. Because rename is atomic, a crash at any point leaves
either the old record (temp not yet renamed) or the new record (rename
done) in place -- never a truncated or partial one. The temp file is
created with O_EXCL so concurrent activations cannot clobber each
other's staging. Activation correctness depends on atomicity, so an
unsupported host fails closed.
"""
if _renameat2 is None:
raise PackError("atomic record replacement unsupported on this host")
marker = os.getpid()
for attempt in range(256):
temp_name = f".{name}.tmp-{marker:x}-{attempt:02x}"
try:
descriptor = os.open(temp_name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC,
0o600, dir_fd=parent_fd)
except FileExistsError:
continue
except OSError as exc:
raise PackError(f"cannot write {name}: {exc}") from exc
break
else:
raise PackError(f"cannot allocate temporary record file for {name}")
try:
_write_all(descriptor, data, name)
os.fsync(descriptor)
except Exception as exc:
os.close(descriptor)
_unlink_quietly_at(parent_fd, temp_name)
raise PackError(f"cannot write {name}: {exc}") from exc
os.close(descriptor)
try:
result = _renameat2(parent_fd, os.fsencode(temp_name), parent_fd, os.fsencode(name), 0)
if result != 0:
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), name)
_fsync_directory_fd(parent_fd)
except Exception as exc:
_unlink_quietly_at(parent_fd, temp_name)
raise PackError(f"cannot atomically replace {name}: {exc}") from exc
actual = _read_file_at(parent_fd, name, len(data))
if actual != data:
raise PackError(f"read-back verification failed for {name}")
def _unlink_quietly_at(parent_fd: int, name: str) -> None:
try:
os.unlink(name, dir_fd=parent_fd)
except OSError:
pass
_INSTALL_LOCK_NAME = ".pyxis-install.lock"
@contextlib.contextmanager
def _install_lock_at(pyxis_fd: int):
"""Hold the exclusive on-disk install lock across preflight-to-activation.
The web flasher serializes concurrent installers through the Web Locks
API; this CLI needs an equivalent because two concurrent --activate
builds could otherwise both pass the preflight and each clobber the
other's records.
The lock is an advisory fcntl.flock(LOCK_EX | LOCK_NB) on a single
well-known file in pyxis-map/, so every builder on the card serializes
on the same inode. flock is the race-free primitive here:
* acquisition is a single atomic kernel operation, so there is no
create-then-write-PID window for a second builder to observe an
empty/partial lock and classify it as stale;
* the kernel releases the lock when the holder exits or is killed,
so a crashed builder cannot wedge the card -- there is no stale
lock to reclaim, and no pid-reuse window;
* the file is deliberately persistent and never unlinked: unlinking a
flock'd file is the classic inode race (a second process would flock
a *new* inode and not conflict with the first's held inode). The
lock file is invisible to the firmware and flasher, which only read
fixed named paths under pyxis-map/ (never a root directory listing).
"""
try:
descriptor = os.open(_INSTALL_LOCK_NAME,
os.O_RDWR | os.O_CREAT | os.O_CLOEXEC, 0o644,
dir_fd=pyxis_fd)
except OSError as exc:
raise PackError(f"cannot open the map-install lock: {exc}") from exc
try:
try:
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as exc:
if exc.errno in (errno.EAGAIN, errno.EACCES, errno.EWOULDBLOCK):
raise PackError("another map installer is already running on "
"this card; wait for it to finish and retry") from None
raise PackError(f"cannot acquire the map-install lock: {exc}") from exc
yield _INSTALL_LOCK_NAME
finally:
# Closing releases the flock. Do NOT unlink: see the docstring.
try:
fcntl.flock(descriptor, fcntl.LOCK_UN)
except OSError:
pass
os.close(descriptor)
# Cross-producer install marker.
#
# The web flasher holds a Web Locks name and this CLI holds an advisory
# flock: two unrelated locking namespaces, so each lock is invisible to
# the other producer. The on-disk marker below is the one coordination
# primitive both producers share -- the flasher writes the same file with
# the same token format (docs/flasher/js/map-installer.js). A marker is a
# small file containing: PYXI 1 <owner> <epoch_ms>.
_INSTALL_MARKER_NAME = ".pyxis-installing"
# A marker written within this window is a live installer. Installations
# longer than this are treated as abandoned (crashed builder, closed tab)
# and reclaimed. A future epoch (writer clock ahead of ours) counts as
# fresh: refusing is the safe direction.
_INSTALL_MARKER_TTL_SECONDS = 900
def _marker_token(owner: str) -> str:
return f"PYXI 1 {owner} {int(time.time() * 1000)}"
def _parse_marker(raw: bytes) -> tuple[str, int] | None:
"""Parse a marker token into (owner, epoch_ms); None if malformed."""
try:
text = raw.decode("ascii")
except UnicodeDecodeError:
return None
parts = text.strip().split(" ")
if len(parts) != 4 or parts[0] != "PYXI" or parts[1] != "1":
return None
if not parts[2] or len(parts[2]) > 64 or not parts[3].isdigit():
return None
return parts[2], int(parts[3])
def _marker_is_fresh(epoch_ms: int) -> bool:
now_ms = int(time.time() * 1000)
if epoch_ms > now_ms:
return True
return (now_ms - epoch_ms) / 1000.0 <= _INSTALL_MARKER_TTL_SECONDS
def _acquire_install_marker(pyxis_fd: int) -> str:
"""Claim the cross-producer install marker; return our token.
Protocol (shared with the web flasher):
* no marker -> create it;
* fresh foreign marker -> refuse: another installer is live;
* stale or malformed -> reclaim (unlink, then create).
After creation the file is read back and compared to our token: a
foreign content means we lost a simultaneous acquire race and we
abort. The marker is best-effort coordination -- the safety net is
the commit-time revalidation in activate_map_set, which refuses to
publish a record over activation state that moved since it was
derived.
"""
owner = f"cli-{os.getpid():x}"
token = _marker_token(owner)
raw = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if raw is not None:
parsed = _parse_marker(raw)
if parsed is not None and _marker_is_fresh(parsed[1]):
raise PackError("another map installer is already running on "
"this card; wait for it to finish and retry")
# Stale or malformed: reclaim. A malformed marker may be a live
# writer mid-write, but the read-back below fails loudly if so,
# and commit-time revalidation covers the rest.
_unlink_quietly_at(pyxis_fd, _INSTALL_MARKER_NAME)
try:
descriptor = os.open(_INSTALL_MARKER_NAME,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC,
0o644, dir_fd=pyxis_fd)
except FileExistsError:
raise PackError("another map installer is already running on "
"this card; wait for it to finish and retry") from None
except OSError as exc:
raise PackError(f"cannot write the map-install marker: {exc}") from exc
try:
_write_all(descriptor, token.encode("ascii"), _INSTALL_MARKER_NAME)
os.fsync(descriptor)
finally:
os.close(descriptor)
actual = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if actual != token.encode("ascii"):
# Someone overwrote (or removed) our marker while we acquired it.
# Never delete a foreign marker: our claim is void, abort.
raise PackError("the map-install marker was contested during "
"acquisition; wait and retry")
_fsync_directory_fd(pyxis_fd)
return token
def _renew_install_marker(pyxis_fd: int, owner: str) -> None:
"""Renew our marker's epoch during a long installation (heartbeat).
A publication can outlive _INSTALL_MARKER_TTL_SECONDS (a full world
pack on slow SD storage); without renewals the marker would age out
and a second producer could reclaim our live claim. The rewrite
keeps our owner identity and advances the epoch; the release path
and the commit-time check compare the owner, so renewed markers are
still recognized as ours.
The renewal is ownership-checked, never a blind overwrite: if the
marker no longer carries OUR owner -- it aged out and another
producer legitimately reclaimed it, was deleted, or is corrupt --
our claim is void. Overwriting a foreign claim would steal the
card mid-install, so the install aborts instead; the (possibly
partially published) pack stays device-harmless and the user
retries after the other installer finishes. A transient I/O error
on the marker read/write also aborts: an unverifiable claim is no
safer than a lost one.
"""
current = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if current is None:
raise PackError("the map-install marker was reclaimed during "
"installation; wait for the other installer to "
"finish and retry")
parsed = _parse_marker(current)
if parsed is None or parsed[0] != owner:
raise PackError("the map-install marker was reclaimed during "
"installation; wait for the other installer to "
"finish and retry")
token = _marker_token(owner)
descriptor = os.open(_INSTALL_MARKER_NAME,
os.O_WRONLY | os.O_TRUNC | os.O_CLOEXEC,
dir_fd=pyxis_fd)
try:
_write_all(descriptor, token.encode("ascii"), _INSTALL_MARKER_NAME)
os.fsync(descriptor)
finally:
os.close(descriptor)
def _release_install_marker(pyxis_fd: int, token: str) -> None:
"""Remove our install marker -- and only ours.
Comparison is by owner identity (the token's owner field), not the
full token: the epoch changes as we renew (heartbeat), so an exact
match would fail once a long install has renewed its marker. If the
marker now carries a DIFFERENT owner (a later installer reclaimed
it after our marker aged out, or a crashed run left a different
claim), deleting it would drop that installer's live claim. Leave
it; the TTL reclaims it.
"""
our_owner = _parse_marker(token.encode("ascii"))
our_owner = our_owner[0] if our_owner is not None else None
raw = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if raw is not None:
current = _parse_marker(raw)
if current is not None and current[0] == our_owner:
try:
os.unlink(_INSTALL_MARKER_NAME, dir_fd=pyxis_fd)
except OSError:
pass
def _verify_activation_state_at(pyxis_fd: int, map_sets_fd: int, *,
slot_raw: list[bytes | None],
style_raw: bytes | None, style_name: str,
marker_token: str | None) -> None:
"""Commit-time revalidation against a concurrent installer.
Re-reads the install marker and the raw activation records
immediately before the record writes. If a cross-producer installer
(web flasher or another builder) committed since the records were
derived -- or holds a fresh install marker that is not ours -- the
activation is aborted before any byte is written. The published pack
is kept; a retry re-derives from the new state and converges (the
pack is device-harmless while it is not named by any record: the
firmware only reads packs enumerated in the active selection).
"""
marker_raw = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if marker_raw is not None:
# Compare by owner, not the full token: we renew the marker's
# epoch during publication (heartbeat), so our own marker may
# carry a newer epoch than the one acquired at install start.
own_owner = None
if marker_token is not None:
parsed_own = _parse_marker(marker_token.encode("ascii"))
own_owner = parsed_own[0] if parsed_own is not None else None
parsed = _parse_marker(marker_raw)
if parsed is not None and _marker_is_fresh(parsed[1]) \
and parsed[0] != own_owner:
raise PackError("another map installer is running on this card; "
"wait for it to finish and retry")
for index, slot_name in enumerate(("active-pack.0", "active-pack.1")):
actual = _read_selection_at(pyxis_fd, slot_name)
if actual != slot_raw[index]:
raise PackError("active map-set records changed during "
"installation; wait for the other installer "
"to finish and retry")
actual_style = _read_selection_at(map_sets_fd, style_name)
if actual_style != style_raw:
raise PackError("installed style record changed during installation; "
"wait for the other installer to finish and retry")
def _remove_tree_at(parent_fd: int, name: str) -> None:
try:
descriptor = os.open(name, _DIRECTORY_FLAGS, dir_fd=parent_fd)
except FileNotFoundError:
return
try:
for entry in os.scandir(descriptor):
info = os.stat(entry.name, dir_fd=descriptor, follow_symlinks=False)
if stat.S_ISDIR(info.st_mode):
_remove_tree_at(descriptor, entry.name)
else:
os.unlink(entry.name, dir_fd=descriptor)
finally:
os.close(descriptor)
os.rmdir(name, dir_fd=parent_fd)
def _temporary_directory(packs_fd: int, pack_id: str) -> tuple[str, int]:
for attempt in range(256):
name = f".{pack_id}.tmp-{os.getpid():x}-{attempt:02x}"
try:
os.mkdir(name, 0o700, dir_fd=packs_fd)
descriptor = os.open(name, _DIRECTORY_FLAGS, dir_fd=packs_fd)
return name, descriptor
except FileExistsError:
continue
raise PackError("cannot allocate temporary output directory")
def _rename_noreplace(parent_fd: int, source: str, destination: str, target: Path) -> None:
if _renameat2 is None:
raise PackError("atomic no-replace unsupported on this host")
result = _renameat2(parent_fd, os.fsencode(source), parent_fd, os.fsencode(destination), 1)
if result == 0:
return
error = ctypes.get_errno()
if error == errno.EEXIST:
raise PackError(f"output already exists: {target}")
if error in (errno.ENOSYS, errno.EINVAL, errno.ENOTSUP, errno.EOPNOTSUPP):
raise PackError("atomic no-replace unsupported on this filesystem")
raise OSError(error, os.strerror(error), target)
def _verify_output_chain(output_root: Path, root_fd: int, pyxis_fd: int, pyxis_identity: tuple[int, ...],
packs_identity: tuple[int, ...]) -> None:
check_fd = _open_path(output_root)
try:
if _identity(os.fstat(check_fd)) != _identity(os.fstat(root_fd)):
raise PackError("output directory changed before publication")
finally:
os.close(check_fd)
try:
pyxis_info = os.stat("pyxis-map", dir_fd=root_fd, follow_symlinks=False)
packs_info = os.stat("packs", dir_fd=pyxis_fd, follow_symlinks=False)
except OSError as exc:
raise PackError(f"output directory changed before publication: {exc}") from exc
if _identity(pyxis_info)[:3] != pyxis_identity[:3] or _identity(packs_info)[:3] != packs_identity[:3]:
raise PackError("output directory changed before publication")
def _preflight_activation(pyxis_fd: int, *, pack_id: str, map_set_id: str, attribution: str) -> None:
"""Reject an activation whose inherited composition exceeds the limit.
Runs before any tile is staged or the pack is published, so a rejected
activation leaves the card exactly as it was found -- no published pack,
no rewritten record. Mirrors the composition rules in activate_map_set.
"""
slot_names = ("active-pack.0", "active-pack.1")
slots: list[dict[str, object] | None] = [None, None]
for index, slot_name in enumerate(slot_names):
raw = _read_selection_at(pyxis_fd, slot_name)
if raw is not None:
slots[index] = decode_active_selection(raw)
if slots[0] is not None and slots[1] is not None \
and slots[0]["generation"] == slots[1]["generation"] and slots[0] != slots[1]:
raise PackError("conflicting active map-set records")
valid = [slot for slot in slots if slot is not None]
highest = max([0] + [cast(int, slot["generation"]) for slot in valid])
if highest == 0xFFFFFFFF:
raise PackError("active selection generation is exhausted")
map_sets_fd, _ = _mkdir_open(pyxis_fd, "map-sets")
try:
style_name = f"{map_set_id}.pmas"
installed_raw = _read_selection_at(map_sets_fd, style_name)
installed = decode_active_selection(installed_raw) if installed_raw is not None else None
if installed is not None:
if installed["format_version"] not in (2, 3) or installed["map_set_id"] != map_set_id \
or installed["attribution"] != attribution:
raise PackError("installed style record does not match selected map set")
composition = installed
if composition is None and valid:
current = max(valid, key=lambda slot: cast(int, slot["generation"]))
if current["format_version"] in (2, 3) and current["map_set_id"] == map_set_id:
composition = current
if composition is not None:
if composition["attribution"] != attribution:
raise PackError("installed map set attribution does not match")
packs = [value for value in cast(list[str], composition["packs"]) if value != pack_id]
packs.insert(0, pack_id)
if len(packs) > MAX_ACTIVE_PACKS:
raise PackError(f"active map-set pack limit exceeded: {len(packs)} > "
f"{MAX_ACTIVE_PACKS}; remove an installed pack before activating")
finally:
os.close(map_sets_fd)
def build_map_pack(source_directory: Path, output_root: Path, *, pack_id: str, name: str,
attribution: str, source: str, license: str,
max_tiles: int = DEFAULT_MAX_TILES, max_bytes: int = DEFAULT_MAX_BYTES,
antimeridian_zooms: set[int] | None = None,
sparse: bool = False, style: str | None = None,
activate: bool = False) -> Path:
if not sys.platform.startswith("linux"):
raise PackError("secure importer supports Linux hosts only")
_validate_metadata(pack_id, name, attribution, source, license)
antimeridian_zooms = set() if antimeridian_zooms is None else set(antimeridian_zooms)
if any(zoom < 0 or zoom > MAX_ZOOM for zoom in antimeridian_zooms):
raise PackError("antimeridian zoom is out of range")
if style is not None:
if style not in STYLE_POLICIES:
raise PackError(f"unsupported map style: {style}")
policy = STYLE_POLICIES[style]
if (attribution, source, license) != (policy["attribution"], policy["source"], policy["license"]):
raise PackError("metadata does not match the selected style policy")
if activate and style is None:
raise PackError("--activate requires --style so the map-set ID is a firmware style policy")
source_directory = Path(source_directory)
output_root = Path(output_root)
target = output_root / "pyxis-map" / "packs" / pack_id
source_fd = _open_path(source_directory)
output_fd = pyxis_fd = packs_fd = stage_fd = pack_fd = -1
temporary = ""
committed = False
try:
tiles = _discover_tiles_fd(source_fd, source_directory, max_tiles, max_bytes)
zooms = sorted({tile.zoom for tile in tiles})
if style is not None:
# Style installs always publish indexless v3 packs, mirroring
# the web flasher: no row spans, no coverage caps.
manifest = serialize_indexless_manifest(pack_id=pack_id, name=name,
attribution=attribution, source=source,
license=license, min_zoom=zooms[0],
max_zoom=zooms[-1], tile_count=len(tiles))
else:
try:
extents = calculate_extents(tiles, antimeridian_zooms)
except PackError as exc:
if not sparse or antimeridian_zooms or not str(exc).startswith("incomplete rectangle"):
raise
try:
row_spans = calculate_row_spans(tiles)
manifest = serialize_sparse_manifest(pack_id=pack_id, name=name,
attribution=attribution, source=source,
license=license, row_spans=row_spans,
tile_count=len(tiles))
except PackError as span_exc:
if "row-span limit" not in str(span_exc):
raise
# Sparse coverage beyond the v2 span cap falls back to
# an indexless v3 pack.
manifest = serialize_indexless_manifest(pack_id=pack_id, name=name,
attribution=attribution, source=source,
license=license, min_zoom=zooms[0],
max_zoom=zooms[-1], tile_count=len(tiles))
else:
manifest = serialize_manifest(pack_id=pack_id, name=name, attribution=attribution, source=source,
license=license, extents=extents, tile_count=len(tiles))
output_fd = _open_path(output_root, create=True)
pyxis_fd, pyxis_identity = _mkdir_open(output_fd, "pyxis-map")
packs_fd, packs_identity = _mkdir_open(pyxis_fd, "packs")
existing = _existing_pack_fd(packs_fd, pack_id)
resumed = False
if existing is not None:
if not activate:
# Without --activate a resume would leave the card exactly
# as it was, so an existing pack is simply refused.
if existing >= 0:
os.close(existing)
existing = -1
raise PackError(f"output already exists: {target}")
# The pack already exists on the card. This is either a retry
# of a run interrupted between pack publication and activation
# (the style/slot records may be stale or split), or an exact
# duplicate run. Mirroring the web flasher, resume is allowed
# only when the published pack is byte-identical to this
# source; then re-activating (or activating for the first
# time) converges the records to this run's generation.
_verify_existing_pack(source_fd, existing, pack_id, manifest, tiles, max_bytes)
pack_fd = existing
resumed = True
if activate:
assert style is not None
# Hold the install lock from the preflight through activation:
# two concurrent builders could otherwise both pass the
# preflight and each clobber the other's records. The cross-
# producer install marker (shared with the web flasher, which
# holds a Web Locks name this flock cannot see) is claimed for
# the same span and released on every exit path.
with _install_lock_at(pyxis_fd):
# Claim the cross-producer install marker FIRST, before any
# staging or publication: a live web-flasher install (which
# holds a Web Locks name this flock cannot see) is announced
# by its marker, and refusing up front avoids the wasted
# tile copies and -- more importantly -- avoids publishing a
# pack that activation would then have to abandon. Stale or
# malformed markers (crashed builder, closed tab) are
# reclaimed here; ours is released on every exit path.
marker_token = _acquire_install_marker(pyxis_fd)
try:
_preflight_activation(pyxis_fd, pack_id=pack_id, map_set_id=style,
attribution=attribution)
if not resumed:
temporary, stage_fd = _temporary_directory(packs_fd, pack_id)
copied = 0
# Heartbeat: renew our marker during long
# publications so a full world pack on slow SD
# storage cannot age out and be reclaimed by a
# second producer mid-install (Greptile round 8).
_marker_owner = _parse_marker(marker_token.encode("ascii"))
_marker_owner = _marker_owner[0] if _marker_owner is not None else None
for tile_index, tile in enumerate(tiles):
copy_tile(source_fd, tile, stage_fd, max_bytes - copied)
copied += tile.size
if _marker_owner is not None and tile_index % 25 == 24:
_renew_install_marker(pyxis_fd, _marker_owner)
if tiles and _marker_owner is not None:
_renew_install_marker(pyxis_fd, _marker_owner)
manifest_fd = os.open("manifest.pmp", os.O_WRONLY | os.O_CREAT | os.O_EXCL
| os.O_CLOEXEC, 0o644, dir_fd=stage_fd)
try:
_write_all(manifest_fd, manifest, "manifest.pmp")
os.fsync(manifest_fd)
finally:
os.close(manifest_fd)
_fsync_directory_fd(stage_fd)
_validate_pack_fd(stage_fd, target, max_bytes)
_verify_output_chain(output_root, output_fd, pyxis_fd, pyxis_identity,
packs_identity)
_rename_noreplace(packs_fd, temporary, pack_id, target)
committed = True
try:
_verify_output_chain(output_root, output_fd, pyxis_fd,
pyxis_identity, packs_identity)
except BaseException as exc:
try:
_remove_tree_at(packs_fd, pack_id)
_fsync_directory_fd(packs_fd)
committed = False
except BaseException as cleanup_error:
raise PublishedDurabilityError(target, OSError(
f"publication location changed and rollback failed: "
f"{cleanup_error}")) from exc
raise PackError("output directory changed during publication; "
"rolled back") from exc
try:
os.close(stage_fd)
except OSError as exc:
stage_fd = -1
raise PublishedDurabilityError(target, exc) from exc
stage_fd = -1
slot_name, _enabled = activate_map_set(pyxis_fd, pack_id=pack_id,
map_set_id=style,
attribution=attribution,
marker_token=marker_token)
finally:
_release_install_marker(pyxis_fd, marker_token)
else:
temporary, stage_fd = _temporary_directory(packs_fd, pack_id)
copied = 0
for tile in tiles:
copy_tile(source_fd, tile, stage_fd, max_bytes - copied)
copied += tile.size
manifest_fd = os.open("manifest.pmp", os.O_WRONLY | os.O_CREAT | os.O_EXCL
| os.O_CLOEXEC, 0o644, dir_fd=stage_fd)
try:
_write_all(manifest_fd, manifest, "manifest.pmp")
os.fsync(manifest_fd)
finally:
os.close(manifest_fd)
_fsync_directory_fd(stage_fd)
_validate_pack_fd(stage_fd, target, max_bytes)
_verify_output_chain(output_root, output_fd, pyxis_fd, pyxis_identity,
packs_identity)
_rename_noreplace(packs_fd, temporary, pack_id, target)
committed = True
try:
_verify_output_chain(output_root, output_fd, pyxis_fd, pyxis_identity,
packs_identity)
except BaseException as exc:
try:
_remove_tree_at(packs_fd, pack_id)
_fsync_directory_fd(packs_fd)
committed = False
except BaseException as cleanup_error:
raise PublishedDurabilityError(target, OSError(
f"publication location changed and rollback failed: "
f"{cleanup_error}")) from exc
raise PackError("output directory changed during publication; "
"rolled back") from exc
try:
os.close(stage_fd)
except OSError as exc:
stage_fd = -1
raise PublishedDurabilityError(target, exc) from exc
stage_fd = -1
try:
_fsync_directory_fd(packs_fd)
except OSError as exc:
raise PublishedDurabilityError(target, exc) from exc
if resumed and pack_fd >= 0:
os.close(pack_fd)
pack_fd = -1
return target
finally:
active_exception = sys.exc_info()[0] is not None
close_error: OSError | None = None
if stage_fd >= 0:
try:
os.close(stage_fd)
except OSError as exc:
close_error = exc
if temporary and not committed and packs_fd >= 0:
_remove_tree_at(packs_fd, temporary)
for descriptor in (pack_fd, packs_fd, pyxis_fd, output_fd, source_fd):
if descriptor >= 0:
try:
os.close(descriptor)
except OSError as exc:
if close_error is None:
close_error = exc
if committed and close_error is not None and not active_exception:
raise PublishedDurabilityError(target, close_error)
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("xyz_directory", type=Path)
parser.add_argument("output_root", type=Path, help="SD-card root under which pyxis-map/packs is created")
parser.add_argument("--pack-id", required=True)
parser.add_argument("--name", required=True)
parser.add_argument("--attribution")
parser.add_argument("--source")
parser.add_argument("--license")
parser.add_argument("--max-tiles", type=int, default=DEFAULT_MAX_TILES)
parser.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES)
parser.add_argument("--antimeridian-zoom", action="append", type=int, default=[])
parser.add_argument("--sparse", action="store_true",
help="explicitly admit sparse state/polygon coverage and emit PMPK v2")
parser.add_argument("--style", choices=sorted(STYLE_POLICIES),
help="firmware style policy; supplies canonical attribution/source/license, "
"emits an indexless PMPK v3 pack")
parser.add_argument("--activate", action="store_true",
help="also publish the v3 active map-set record (active-pack slot + map-sets/<style>.pmas)")
return parser
def main(argv: list[str] | None = None) -> int:
arguments = _parser().parse_args(argv)
if arguments.style is not None:
policy = STYLE_POLICIES[arguments.style]
attribution = arguments.attribution or policy["attribution"]
source = arguments.source or policy["source"]
license = arguments.license or policy["license"]
else:
if not (arguments.attribution and arguments.source and arguments.license):
print("error: --attribution, --source, and --license are required unless --style is given",
file=sys.stderr)
return 2
attribution, source, license = arguments.attribution, arguments.source, arguments.license
try:
target = build_map_pack(arguments.xyz_directory, arguments.output_root,
pack_id=arguments.pack_id, name=arguments.name,
attribution=attribution, source=source,
license=license, max_tiles=arguments.max_tiles,
max_bytes=arguments.max_bytes,
antimeridian_zooms=set(arguments.antimeridian_zoom),
sparse=arguments.sparse, style=arguments.style,
activate=arguments.activate)
except PublishedDurabilityError as exc:
print(f"error: {exc}", file=sys.stderr)
return 3
except (PackError, OSError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
print(target)
return 0
if __name__ == "__main__":
raise SystemExit(main())