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
- 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.
- Verify you are passing the raw compressed .rnote bytes, not an already-decompressed stream or a different file.
- Confirm the file path resolves to a real synced file (not a cloud placeholder) before loading.
- 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
- Verify file size and gzip magic bytes (1f 8b) before calling load_from_bytes.
- Check download/sync completion status before opening .rnote files from cloud storage.
- Never pass partially-read buffers; read the whole file with std::fs::read.
- Keep automatic backups so truncated files can be replaced.
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
- no value `value` in JSON object of `stroke_components`…
- document has no value `layout`.
- engine snapshot does not contain 'stroke_components'.
- stroke component does not contain 'value'.
- value is not a JSON object.
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)