use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use crate::errors::DataProfilerError;
const PARQUET_MAGIC: &[u8; 4] = b"PAR1";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BinarySignature {
Gzip,
Zstd,
Zip,
Parquet,
}
impl BinarySignature {
fn from_prefix(head: &[u8]) -> Option<Self> {
if head.starts_with(&[0x1f, 0x8b]) {
Some(Self::Gzip)
} else if head.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) {
Some(Self::Zstd)
} else if head.starts_with(&[0x50, 0x4b, 0x03, 0x04]) {
Some(Self::Zip)
} else {
None
}
}
pub fn description(self) -> &'static str {
match self {
Self::Gzip => "gzip-compressed",
Self::Zstd => "zstd-compressed",
Self::Zip => "a zip archive",
Self::Parquet => "a Parquet file",
}
}
pub fn suggestion(self, path: &str) -> String {
match self {
Self::Gzip => {
format!("Decompress it first (e.g. `gunzip -k '{path}'`) and profile the result.")
}
Self::Zstd => {
format!("Decompress it first (e.g. `zstd -d '{path}'`) and profile the result.")
}
Self::Zip => format!(
"Extract the file to profile (e.g. `unzip '{path}'`) and profile it. A spreadsheet \
(.xlsx, .ods) is a zip archive too: export the sheet as CSV first."
),
Self::Parquet => parquet_suggestion(),
}
}
}
#[cfg(feature = "parquet")]
fn parquet_suggestion() -> String {
"Read it as Parquet: give it a `.parquet` extension, or select the format \
(`.format(FileFormat::Parquet)` in Rust, `format=\"parquet\"` in Python)."
.to_string()
}
#[cfg(not(feature = "parquet"))]
fn parquet_suggestion() -> String {
"This build does not read Parquet; rebuild with the `parquet` feature to profile it."
.to_string()
}
pub fn sniff_binary_file(path: &Path) -> Option<BinarySignature> {
let mut file = File::open(path).ok()?;
let mut head = Vec::with_capacity(4);
(&mut file).take(4).read_to_end(&mut head).ok()?;
if let Some(signature) = BinarySignature::from_prefix(&head) {
return Some(signature);
}
if head != PARQUET_MAGIC {
return None;
}
if file.metadata().ok()?.len() < 8 {
return None;
}
let mut tail = [0u8; 4];
file.seek(SeekFrom::End(-4)).ok()?;
file.read_exact(&mut tail).ok()?;
(&tail == PARQUET_MAGIC).then_some(BinarySignature::Parquet)
}
pub fn reject_binary_input(path: &Path) -> Result<(), DataProfilerError> {
match sniff_binary_file(path) {
Some(signature) => Err(DataProfilerError::binary_input(path, signature)),
None => Ok(()),
}
}