{"record":{"id":"dd3f9f28b29ce50c","repo":"ducaale/xh","slug":"truncated-zstd-skippable-frame","errorCode":null,"errorMessage":"truncated zstd skippable frame","messagePattern":"truncated zstd skippable frame","errorType":"error_code","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"src/decoder.rs","lineNumber":262,"sourceCode":"            match self.state {\n                ZstdDecoderState::NeedFrame => {\n                    if self.reader.fill_buf()?.is_empty() {\n                        self.state = ZstdDecoderState::Finished;\n                        return Ok(0);\n                    }\n\n                    match self.decoder.reset(&mut self.reader) {\n                        Ok(()) => self.state = ZstdDecoderState::Decoding,\n                        Err(FrameDecoderError::ReadFrameHeaderError(\n                            ReadFrameHeaderError::SkipFrame { length, .. },\n                        )) => {\n                            let length = u64::from(length);\n                            let copied = {\n                                let mut payload = self.reader.by_ref().take(length);\n                                io::copy(&mut payload, &mut io::sink())?\n                            };\n                            if copied != length {\n                                return Err(io::Error::new(\n                                    io::ErrorKind::UnexpectedEof,\n                                    \"truncated zstd skippable frame\",\n                                ));\n                            }\n                        }\n                        Err(err) => return Err(io::Error::other(err)),\n                    }\n                }\n                ZstdDecoderState::Decoding => {\n                    while self.decoder.can_collect() < buf.len() && !self.decoder.is_finished() {\n                        let additional_bytes = buf.len() - self.decoder.can_collect();\n                        self.decoder\n                            .decode_blocks(\n                                &mut self.reader,\n                                BlockDecodingStrategy::UptoBytes(additional_bytes),\n                            )\n                            .map_err(io::Error::other)?;\n                    }","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/ducaale/xh/blob/2404aceecc08b0b2d100fedc96f57745cd5904dc/src/decoder.rs#L244-L280","documentation":"The zstd decoder encountered a skippable frame (a non-data frame decoders should pass over) whose declared payload length extends past the end of the input stream. The reader copied fewer bytes than the frame header promised, which can only mean the compressed data was cut short mid-frame. The library surfaces this as UnexpectedEof so callers can distinguish 'input ended inside a skippable frame' from a clean end-of-stream.","triggerScenarios":"Calling Decoder::read (e.g. streaming decode of a .zst file or socket) on a truncated input: the file was cut off, a download was interrupted, or the writer died before flushing the full skippable frame declared in its header.","commonSituations":"Partially downloaded or interrupted .zst files; piping from a producer that crashed or was SIGPIPE'd; storing compressed data in a store that truncates trailing bytes; concatenating streams where the tail frame is incomplete.","solutions":["Verify the source .zst file/stream is complete and re-download or re-fetch it","Check the producer: ensure the compressor finished writing and the full stream was flushed/closed","Validate file size against the expected size (e.g. Content-Length or checksum) before decoding","If the input is intentionally truncated, treat UnexpectedEof from read as expected and handle it explicitly"],"exampleFix":"// before\nlet mut out = Vec::new();\ndecoder.read_to_end(&mut out)?; // panics/errors on truncated file\n\n// after\nmatch decoder.read_to_end(&mut out) {\n    Ok(_) => {},\n    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {\n        eprintln!(\"compressed input is truncated; refetching\");\n        // refetch or recover\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"try-catch","validationCode":"// Check input size against expected before decoding\nlet meta = std::fs::metadata(&path)?;\nif meta.len() < expected_min_size {\n    return Err(format!(\"{} is truncated ({} bytes, expected >= {})\", path.display(), meta.len(), expected_min_size));\n}","typeGuard":"fn looks_truncated(e: &std::io::Error) -> bool {\n    e.kind() == std::io::ErrorKind::UnexpectedEof\n}","tryCatchPattern":"match decoder.read_to_end(&mut out) {\n    Ok(_) => Ok(out),\n    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {\n        // recover: refetch source or use last-good cached copy\n        Err(DecodeError::Truncated(e))\n    }\n    Err(e) => Err(e.into()),\n}","preventionTips":["Verify checksums or Content-Length on downloaded .zst files before decoding","Ensure producers close/flush compressed streams before consumers read","Use resumable downloads and retry on partial transfers","Wrap long-lived readers (sockets/pipes) with timeouts so truncation is detected early"],"tags":["io","compression","zstd","truncated-input","eof"],"backgroundTag":"file-read-failed","analyzedSha":"2404aceecc08b0b2d100fedc96f57745cd5904dc","analyzedAt":"2026-09-13T19:13:33.814Z","contentChangedAt":"2026-09-13T19:13:33.814Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}