astrid-runtime/astrid · error

principal-home migration receipt exceeds

Error message

principal-home migration receipt exceeds {MAX_RECEIPT_INDEX_BYTES} bytes

What it means

`read_receipt` loads a principal-home migration receipt index and enforces a hard cap of MAX_RECEIPT_INDEX_BYTES on the file size before JSON parsing. This bounds memory and rejects oversized (possibly tampered or foreign) files. When the receipt file on disk exceeds the cap, the read fails with this InvalidData io::Error instead of attempting to parse it.

Solutions

  1. Inspect the receipt file at the path; if it is corrupted or contains concatenated/foreign data, remove it and re-run the migration so a fresh canonical receipt is written.
  2. Verify the receipt is canonical JSON of a MigrationReceipt within the size cap; rebuild it with write_receipt if it was produced by an older/larger format.
  3. Do not simply raise MAX_RECEIPT_INDEX_BYTES unless you also control all readers — it is a DoS/tamper bound; if raised, do so consistently across versions.
  4. Check for processes writing to the migrations directory (backups, log redirections) and exclude it from such tools.

Example fix

// before
cat receipt-one.json receipt-two.json > .astrid-migrations/receipt-<uid>.json // oversized concatenated receipt
// after
let receipt = write_receipt(path, &current_receipt)?; // single canonical receipt within the byte cap
Defensive patterns

Strategy: try-catch

Validate before calling

// check size before asking the library to parse the receipt
fn receipt_is_plausible(path: &Path) -> io::Result<bool> {
    Ok(fs::metadata(path)?.len() <= MAX_RECEIPT_INDEX_BYTES as u64)
}

Try / catch

match read_receipt(&receipt_path) {
    Err(e) if e.kind() == io::ErrorKind::InvalidData && e.to_string().contains("receipt exceeds") => {
        eprintln!("receipt file oversized/corrupted, remove and re-run migration: {e}");
    },
    Err(e) => return Err(e),
    Ok(None) => eprintln!("no prior receipt"),
    Ok(Some(receipt)) => { /* proceed */ },
}

Prevention

When it happens

Trigger: Any caller (`migrate_one_principal`, `retire_one_receipted_source`, `verify_migrated_legacy_principal_sources_retired`) invoking `read_receipt` on a receipt file whose byte length exceeds MAX_RECEIPT_INDEX_BYTES — e.g. a corrupted, concatenated, or hand-edited receipt, or a file accidentally written to the receipt path that is not a real receipt.

Common situations: A backup/restore tool appending multiple receipts into one file; logs or crash dumps written over the receipt path; an old writer producing larger receipts than the current MAX_RECEIPT_INDEX_BYTES allows; deliberate tampering attempts (the cap is a security bound).

Understand the failure class

Background: "File too large" / "file size exceeds limit" errors: why libraries cap file sizes and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/f9ce0ea16e040777. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-kernel/src/principal_home_migration/receipts.rs:163

            &receipt_path(home, uid),
            "receipt entry count does not match its pages",
        ));
    }
    Ok(())
}

pub(super) fn read_receipt(path: &Path) -> io::Result<Option<MigrationReceipt>> {
    match fs::symlink_metadata(path) {
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(error) => Err(error),
        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
            Err(invalid_source(path, "receipt is not a regular file"))
        },
        Ok(_) => {
            astrid_core::platform_fs::validate_private_file(path)?;
            let bytes = fs::read(path)?;
            if bytes.len() > MAX_RECEIPT_INDEX_BYTES {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!(
                        "principal-home migration receipt exceeds {MAX_RECEIPT_INDEX_BYTES} bytes"
                    ),
                ));
            }
            let receipt: MigrationReceipt = serde_json::from_slice(&bytes).map_err(|error| {
                io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("invalid principal-home migration receipt: {error}"),
                )
            })?;
            if receipt.page_count.get() > receipt.entry_count.get()
                || (receipt.entry_count == EntryCount::ZERO
                    && receipt.page_count != PageCount::ZERO)
            {
                return Err(invalid_source(path, "receipt page count is not canonical"));
            }

View on GitHub (pinned to affd8760f4)