mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-02 02:08:41 +00:00
ff28b79088
* fix(s3): harden Snowball extract error boundaries * fix(s3): close Snowball extract compatibility gaps * fix(s3): verify Snowball request body completion * test(s3): reject forged Snowball streaming signatures * build(deps): pin Snowball archive parser limits * fix(s3): preserve Snowball trailer and member errors * docs(architecture): register Snowball tar fork cleanup * refactor(s3): route Snowball errors through object boundary * ci(deps): allow pinned tokio-tar source * ci(e2e): refresh Snowball smoke selection
244 lines
8.4 KiB
Rust
244 lines
8.4 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use async_compression::tokio::bufread::{BzDecoder, GzipDecoder, XzDecoder, ZlibDecoder, ZstdDecoder};
|
|
use thiserror::Error;
|
|
use tokio::io::{AsyncRead, BufReader};
|
|
|
|
pub type Result<T> = std::result::Result<T, ZipError>;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ZipError {
|
|
#[error("unsupported {operation} for format {format:?}")]
|
|
UnsupportedFormat {
|
|
format: CompressionFormat,
|
|
operation: &'static str,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum CompressionFormat {
|
|
Gzip,
|
|
Bzip2,
|
|
Zip,
|
|
Xz,
|
|
Zlib,
|
|
Zstd,
|
|
Tar,
|
|
Unknown,
|
|
}
|
|
|
|
/// Archive guardrails. The values are carried here so every archive caller
|
|
/// shares one default policy; enforcement belongs to the caller, which maps a
|
|
/// breach onto its own protocol error.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ArchiveLimits {
|
|
pub max_entries: usize,
|
|
pub max_entry_size: u64,
|
|
pub max_total_unpacked_size: u64,
|
|
pub max_decoded_size: u64,
|
|
pub max_path_length: usize,
|
|
pub max_pax_metadata_size: u64,
|
|
pub max_total_pax_metadata_size: u64,
|
|
pub max_pax_metadata_records: usize,
|
|
pub max_total_pax_metadata_records: usize,
|
|
pub validate_entry_paths: bool,
|
|
}
|
|
|
|
impl Default for ArchiveLimits {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_entries: 100_000,
|
|
max_entry_size: 1_073_741_824,
|
|
max_total_unpacked_size: 10_737_418_240,
|
|
max_decoded_size: 11_811_160_064,
|
|
max_path_length: 1024,
|
|
max_pax_metadata_size: 1_048_576,
|
|
max_total_pax_metadata_size: 67_108_864,
|
|
max_pax_metadata_records: 4_096,
|
|
max_total_pax_metadata_records: 100_000,
|
|
validate_entry_paths: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompressionFormat {
|
|
/// Map an archive extension onto the stream codec needed to read it.
|
|
/// Tar-family suffixes (`tgz`, `tbz2`, `txz`, `tzst`, ...) resolve to their
|
|
/// codec because the tar container itself is read from the decoded stream.
|
|
pub fn from_extension(ext: &str) -> Self {
|
|
match ext.to_ascii_lowercase().as_str() {
|
|
"gz" | "gzip" | "tgz" => CompressionFormat::Gzip,
|
|
"bz2" | "bzip2" | "tbz" | "tbz2" => CompressionFormat::Bzip2,
|
|
"xz" | "txz" => CompressionFormat::Xz,
|
|
"zlib" | "zz" => CompressionFormat::Zlib,
|
|
"zst" | "zstd" | "tzst" => CompressionFormat::Zstd,
|
|
"tar" => CompressionFormat::Tar,
|
|
"zip" => CompressionFormat::Zip,
|
|
_ => CompressionFormat::Unknown,
|
|
}
|
|
}
|
|
|
|
pub fn extension(&self) -> &'static str {
|
|
match self {
|
|
CompressionFormat::Gzip => "gz",
|
|
CompressionFormat::Bzip2 => "bz2",
|
|
CompressionFormat::Zip => "zip",
|
|
CompressionFormat::Xz => "xz",
|
|
CompressionFormat::Zlib => "zlib",
|
|
CompressionFormat::Zstd => "zst",
|
|
CompressionFormat::Tar => "tar",
|
|
CompressionFormat::Unknown => "",
|
|
}
|
|
}
|
|
|
|
pub fn get_decoder<R>(&self, input: R) -> Result<Box<dyn AsyncRead + Send + Unpin>>
|
|
where
|
|
R: AsyncRead + Send + Unpin + 'static,
|
|
{
|
|
let reader = BufReader::new(input);
|
|
|
|
let decoder: Box<dyn AsyncRead + Send + Unpin + 'static> = match self {
|
|
CompressionFormat::Gzip => {
|
|
let mut decoder = GzipDecoder::new(reader);
|
|
decoder.multiple_members(true);
|
|
Box::new(decoder)
|
|
}
|
|
CompressionFormat::Bzip2 => {
|
|
let mut decoder = BzDecoder::new(reader);
|
|
decoder.multiple_members(true);
|
|
Box::new(decoder)
|
|
}
|
|
CompressionFormat::Zlib => {
|
|
let mut decoder = ZlibDecoder::new(reader);
|
|
decoder.multiple_members(true);
|
|
Box::new(decoder)
|
|
}
|
|
CompressionFormat::Xz => {
|
|
let mut decoder = XzDecoder::new(reader);
|
|
decoder.multiple_members(true);
|
|
Box::new(decoder)
|
|
}
|
|
CompressionFormat::Zstd => {
|
|
let mut decoder = ZstdDecoder::new(reader);
|
|
decoder.multiple_members(true);
|
|
Box::new(decoder)
|
|
}
|
|
CompressionFormat::Tar => Box::new(reader),
|
|
CompressionFormat::Zip => {
|
|
return Err(ZipError::UnsupportedFormat {
|
|
format: *self,
|
|
operation: "stream decoding",
|
|
});
|
|
}
|
|
CompressionFormat::Unknown => {
|
|
return Err(ZipError::UnsupportedFormat {
|
|
format: *self,
|
|
operation: "decoding",
|
|
});
|
|
}
|
|
};
|
|
|
|
Ok(decoder)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use async_compression::tokio::write::GzipEncoder;
|
|
use std::mem::size_of;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
#[test]
|
|
fn test_compression_format_from_extension() {
|
|
assert_eq!(CompressionFormat::from_extension("gz"), CompressionFormat::Gzip);
|
|
assert_eq!(CompressionFormat::from_extension("ZIP"), CompressionFormat::Zip);
|
|
assert_eq!(CompressionFormat::from_extension("tzst"), CompressionFormat::Zstd);
|
|
assert_eq!(CompressionFormat::from_extension("txt"), CompressionFormat::Unknown);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compression_format_size_is_small() {
|
|
assert!(size_of::<CompressionFormat>() <= 8);
|
|
assert!(size_of::<Option<CompressionFormat>>() <= 16);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_decoder_round_trips_gzip_stream() {
|
|
let mut encoder = GzipEncoder::new(Vec::new());
|
|
encoder.write_all(b"payload").await.expect("gzip encode should succeed");
|
|
encoder.shutdown().await.expect("gzip encoder shutdown should succeed");
|
|
|
|
let mut decoder = CompressionFormat::Gzip
|
|
.get_decoder(std::io::Cursor::new(encoder.into_inner()))
|
|
.expect("gzip decoder should be created");
|
|
let mut decoded = Vec::new();
|
|
decoder.read_to_end(&mut decoded).await.expect("gzip decode should succeed");
|
|
|
|
assert_eq!(decoded, b"payload");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_decoder_consumes_concatenated_gzip_members() {
|
|
async fn gzip_member(payload: &[u8]) -> Vec<u8> {
|
|
let mut encoder = GzipEncoder::new(Vec::new());
|
|
encoder.write_all(payload).await.expect("gzip encode should succeed");
|
|
encoder.shutdown().await.expect("gzip encoder shutdown should succeed");
|
|
encoder.into_inner()
|
|
}
|
|
|
|
let mut encoded = gzip_member(b"first-").await;
|
|
encoded.extend(gzip_member(b"second").await);
|
|
let mut decoder = CompressionFormat::Gzip
|
|
.get_decoder(std::io::Cursor::new(encoded))
|
|
.expect("gzip decoder should be created");
|
|
let mut decoded = Vec::new();
|
|
|
|
decoder
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect("concatenated gzip members should decode");
|
|
|
|
assert_eq!(decoded, b"first-second");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_decoder_rejects_zip_and_unknown_formats() {
|
|
let zip_err = CompressionFormat::Zip
|
|
.get_decoder(std::io::Cursor::new(Vec::<u8>::new()))
|
|
.err()
|
|
.expect("zip stream decoding should be rejected");
|
|
assert!(matches!(
|
|
zip_err,
|
|
ZipError::UnsupportedFormat {
|
|
format: CompressionFormat::Zip,
|
|
operation: "stream decoding",
|
|
}
|
|
));
|
|
|
|
let unknown_err = CompressionFormat::Unknown
|
|
.get_decoder(std::io::Cursor::new(Vec::<u8>::new()))
|
|
.err()
|
|
.expect("unknown format decoding should be rejected");
|
|
assert!(matches!(
|
|
unknown_err,
|
|
ZipError::UnsupportedFormat {
|
|
format: CompressionFormat::Unknown,
|
|
operation: "decoding",
|
|
}
|
|
));
|
|
}
|
|
}
|