flxzt/rnote · error · anyhow::Error

Invalid file

Error message

Invalid file

What it means

Rnote stores its compressed file as gzip-compressed JSON with a 4-byte little-endian trailing size trailer used to preallocate the output buffer. This error is thrown in `decompress_from_gzip` (called from `load_from_bytes`) when the input is shorter than 4 bytes, so the trailer size cannot be read — meaning the input cannot be a valid .rnote file.

Solutions

  1. Check the input file size with `ls -l` / `stat`; if it is under 4 bytes, re-obtain the real .rnote file from a backup or re-download it.
  2. Verify you are passing the raw compressed .rnote bytes, not an already-decompressed stream or a different file.
  3. Confirm the file path resolves to a real synced file (not a cloud placeholder) before loading.
  4. Validate the input with a quick gzip header check (`file` command or first two bytes 0x1f 0x8b) before calling `load_from_bytes`.

Example fix

// before: load whatever bytes are available
let bytes = std::fs::read(path)?;
engine.load_from_bytes(&bytes).await?;
// after: guard against truncated/empty input
let bytes = std::fs::read(path)?;
if bytes.len() < 4 || &bytes[..2] != b"\x1f\x8b" {
    anyhow::bail!("not a valid .rnote file (too short or not gzip)");
}
engine.load_from_bytes(&bytes).await?;
Defensive patterns

Strategy: validation

Validate before calling

// validate before loading
fn looks_like_rnote(bytes: &[u8]) -> bool {
    bytes.len() >= 4 && bytes.starts_with(b"\x1f\x8b")
}
// usage: ensure std::fs::read returned the full compressed file
if !looks_like_rnote(&bytes) {
    anyhow::bail!("file is too short or not gzip-compressed; refusing to load");
}

Type guard

fn is_gzip_buffer(bytes: &[u8]) -> bool {
    bytes.len() >= 4 && bytes[0] == 0x1f && bytes[1] == 0x8b
}

Try / catch

match engine.load_from_bytes(&bytes).await {
    Ok(()) => /* loaded */,
    Err(e) if e.to_string().contains("Invalid file") => {
        // input truncated or wrong file; re-download / restore from backup
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `load_from_bytes` (or `decompress_from_gzip`) with fewer than 4 bytes of input: an empty file, a placeholder/URL stub, a truncated download, or passing the wrong buffer (e.g. an already-decompressed stream or metadata instead of file contents).

Common situations: Downloads that were cut off or saved as 0-byte/HTML error pages, cloud-sync placeholders not yet materialized, opening a wrong file type that happens to reach the loader, or callers that read only part of the file.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of flxzt/rnote@bbc5354502 (2026-09-08). Data as JSON: /api/errors/e90e837a4ab971ec. Report an issue: GitHub.

Appendix: source

Thrown at crates/rnote-engine/src/fileformats/rnoteformat/mod.rs:52

    Ok(encoder.finish()?)
}

/// Decompress from gzip.
fn decompress_from_gzip(compressed: &[u8]) -> Result<Vec<u8>, anyhow::Error> {
    // Optimization for the gzip format, defined by RFC 1952
    // capacity of the vector defined by the size of the uncompressed data
    // given in little endian format, by the last 4 bytes of "compressed"
    //
    //   ISIZE (Input SIZE)
    //     This contains the size of the original (uncompressed) input data modulo 2^32.
    let mut bytes: Vec<u8> = {
        let mut decompressed_size: [u8; 4] = [0; 4];
        let idx_start = compressed
            .len()
            .checked_sub(4)
            // only happens if the file has less than 4 bytes
            .ok_or_else(|| {
                anyhow::anyhow!("Invalid file")
                    .context("Failed to get the size of the decompressed data")
            })?;
        decompressed_size.copy_from_slice(&compressed[idx_start..]);
        // u32 -> usize to avoid issues on 32-bit architectures
        // also more reasonable since the uncompressed size is given by 4 bytes
        Vec::with_capacity(u32::from_le_bytes(decompressed_size) as usize)
    };

    let mut decoder = flate2::read::MultiGzDecoder::new(compressed);
    decoder.read_to_end(&mut bytes)?;
    Ok(bytes)
}

/// The rnote file wrapper.
///
/// Used to extract and match the version up front, before deserializing the data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "rnotefile_wrapper")]

View on GitHub (pinned to bbc5354502)