use core::fmt;
use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
pub const MAGIC: &[u8; 3] = b"AES";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Version {
V0,
V1,
V2,
V3,
}
impl Version {
pub const LATEST: Version = Version::V3;
pub fn from_u8(version: u8) -> Option<Version> {
match version {
0 => Some(Version::V0),
1 => Some(Version::V1),
2 => Some(Version::V2),
3 => Some(Version::V3),
_ => None,
}
}
pub fn as_u8(self) -> u8 {
match self {
Version::V0 => 0,
Version::V1 => 1,
Version::V2 => 2,
Version::V3 => 3,
}
}
pub fn min_stream_len(self) -> usize {
match self {
Version::V0 => 53,
Version::V1 => 134,
Version::V2 => 136,
Version::V3 => 155,
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AES Crypt stream format version {}", self.as_u8())
}
}
pub fn from_header(header: &[u8]) -> Option<Version> {
match header {
[b'A', b'E', b'S', version, ..] => Version::from_u8(*version),
_ => None,
}
}
pub fn from_bytes(data: &[u8]) -> Option<Version> {
from_header(data).filter(|v| data.len() >= v.min_stream_len())
}
pub fn from_reader<R: Read>(mut reader: R) -> io::Result<Option<Version>> {
let mut header = [0u8; 4];
match reader.read_exact(&mut header) {
Ok(()) => Ok(from_header(&header)),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(e),
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> io::Result<Option<Version>> {
let file = File::open(path)?;
let len = file.metadata()?.len();
Ok(from_reader(file)?.filter(|v| len >= v.min_stream_len() as u64))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AesFile {
version: Version,
path: PathBuf,
}
impl AesFile {
pub fn version(&self) -> Version {
self.version
}
pub fn path(&self) -> &Path {
&self.path
}
}
pub fn get_file<P: AsRef<Path>>(path: P) -> Option<AesFile> {
let path = path.as_ref();
let version = from_file(path).ok()??;
Some(AesFile { version, path: path.to_path_buf() })
}
#[cfg(test)]
mod tests {
use super::*;
fn stream(version: u8, len: usize) -> Vec<u8> {
let mut data = vec![b'A', b'E', b'S', version, 0];
data.resize(len, 0);
data
}
#[test]
fn detects_every_version_at_minimum_length() {
for v in [Version::V0, Version::V1, Version::V2, Version::V3] {
let len = v.min_stream_len();
assert_eq!(from_bytes(&stream(v.as_u8(), len)), Some(v));
assert_eq!(from_bytes(&stream(v.as_u8(), len - 1)), None);
assert_eq!(from_header(&stream(v.as_u8(), 4)), Some(v));
}
}
#[test]
fn rejects_non_aes_crypt_data() {
assert_eq!(from_bytes(&stream(4, 1000)), None);
assert_eq!(from_header(b"AES"), None);
assert_eq!(from_header(b"aes\x02"), None);
assert_eq!(from_header(b""), None);
}
#[test]
fn reader_detection() {
assert_eq!(from_reader(&b"AES\x03"[..]).unwrap(), Some(Version::V3));
assert_eq!(from_reader(&b"AES"[..]).unwrap(), None);
assert_eq!(from_reader(&b"PK\x03\x04"[..]).unwrap(), None);
}
#[test]
fn file_detection() {
let dir = std::env::temp_dir().join(format!("aescry-detect-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let write = |name: &str, bytes: &[u8]| {
let path = dir.join(name);
std::fs::write(&path, bytes).unwrap();
path
};
let v2 = write("v2.aes", &stream(2, 136));
let v3_short = write("v3short.aes", &stream(3, 154));
let bad_version = write("v9.aes", &stream(9, 200));
let truncated = write("short.aes", b"AES");
let plain = write("plain.txt", b"hello");
let file = get_file(&v2).unwrap();
assert_eq!(file.version(), Version::V2);
assert_eq!(file.path(), v2.as_path());
assert!(get_file(&v3_short).is_none());
assert!(get_file(&bad_version).is_none());
assert!(get_file(&truncated).is_none());
assert!(get_file(&plain).is_none());
assert!(get_file(dir.join("missing.aes")).is_none());
assert!(from_file(dir.join("missing.aes")).is_err());
std::fs::remove_dir_all(&dir).unwrap();
}
}