use crate::reload_check::Refusal;
const KIND: &str = "reference_images";
pub const HEAD: usize = 1 << 20;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Format {
Png,
Jpeg,
Gif,
Webp,
}
impl Format {
pub fn word(self) -> &'static str {
match self {
Format::Png => "png",
Format::Jpeg => "jpeg",
Format::Gif => "gif",
Format::Webp => "webp",
}
}
fn of_extension(ext: &str) -> Option<Format> {
match ext {
"png" => Some(Format::Png),
"jpg" | "jpeg" => Some(Format::Jpeg),
"gif" => Some(Format::Gif),
"webp" => Some(Format::Webp),
_ => None,
}
}
fn sniff(bytes: &[u8]) -> Option<Format> {
if bytes.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) {
Some(Format::Png)
} else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
Some(Format::Jpeg)
} else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
Some(Format::Gif)
} else if bytes.len() >= 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
Some(Format::Webp)
} else {
None
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Header {
pub format: Format,
pub width: u32,
pub height: u32,
}
type Fault = (&'static str, String);
fn short(field: &'static str, what: &str) -> Fault {
(field, format!("the file ends inside the header, before the {what}"))
}
fn start_of(bytes: &[u8]) -> String {
let head = &bytes[..bytes.len().min(8)];
if head.iter().all(|b| (0x20..0x7F).contains(b)) {
format!("\"{}\"", String::from_utf8_lossy(head))
} else {
head.iter().map(|b| format!("{b:02x}")).collect::<Vec<_>>().join(" ")
}
}
pub fn read(file: &str, bytes: &[u8]) -> Result<Header, Refusal> {
let refuse = |(field, reason): Fault| Refusal { kind: KIND, file: file.to_owned(), field: Some(field.to_owned()), reason };
let name = file.rsplit('/').next().unwrap_or(file);
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase()).unwrap_or_default();
let Some(named) = Format::of_extension(&ext) else {
return Err(refuse(("extension", format!("`{name}` does not end in png, jpg, jpeg, webp or gif"))));
};
if bytes.is_empty() {
return Err(refuse(("signature", "the file is empty".to_owned())));
}
let Some(found) = Format::sniff(bytes) else {
return Err(refuse(("signature", format!("the file starts {}, which is no png, jpeg, gif or webp picture", start_of(bytes)))));
};
if found != named {
return Err(refuse(("format", format!("the file is a {} picture and its name says {}", found.word(), named.word()))));
}
let (width, height) = match found {
Format::Png => png(bytes),
Format::Jpeg => jpeg(bytes),
Format::Gif => gif(bytes),
Format::Webp => webp(bytes),
}
.map_err(refuse)?;
for (field, size) in [("width", width), ("height", height)] {
if size == 0 {
return Err(refuse((field, format!("the {field} is 0 pixels"))));
}
}
Ok(Header { format: found, width, height })
}
fn be32(b: &[u8]) -> u32 {
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
}
fn le16(b: &[u8]) -> u32 {
u32::from(u16::from_le_bytes([b[0], b[1]]))
}
fn png(b: &[u8]) -> Result<(u32, u32), Fault> {
if b.len() < 16 {
return Err(short("width", "width"));
}
if &b[12..16] != b"IHDR" {
return Err(("header", format!("the first chunk is `{}`, not IHDR", String::from_utf8_lossy(&b[12..16]))));
}
if b.len() < 20 {
return Err(short("width", "width"));
}
if b.len() < 24 {
return Err(short("height", "height"));
}
Ok((be32(&b[16..20]), be32(&b[20..24])))
}
fn gif(b: &[u8]) -> Result<(u32, u32), Fault> {
if b.len() < 8 {
return Err(short("width", "width"));
}
if b.len() < 10 {
return Err(short("height", "height"));
}
Ok((le16(&b[6..8]), le16(&b[8..10])))
}
fn jpeg(b: &[u8]) -> Result<(u32, u32), Fault> {
let no_frame = || short("width", "frame header that names the width");
let mut i = 2;
loop {
if i >= b.len() {
return Err(no_frame());
}
if b[i] != 0xFF {
return Err(("header", format!("byte {i} is {:02x}, where a marker starts with ff", b[i])));
}
while i < b.len() && b[i] == 0xFF {
i += 1;
}
let Some(&marker) = b.get(i) else { return Err(no_frame()) };
i += 1;
match marker {
0x01 | 0xD0..=0xD8 => continue,
0xD9 | 0xDA => return Err(("width", "a scan begins before any frame header names the width".to_owned())),
_ => {}
}
if i + 2 > b.len() {
return Err(no_frame());
}
let length = usize::from(u16::from_be_bytes([b[i], b[i + 1]]));
if length < 2 {
return Err(("header", format!("the segment at byte {} is {length} bytes long, which cannot hold its own length", i - 1)));
}
if matches!(marker, 0xC0..=0xCF) && !matches!(marker, 0xC4 | 0xC8 | 0xCC) {
if b.len() < i + 5 {
return Err(short("height", "height"));
}
if b.len() < i + 7 {
return Err(short("width", "width"));
}
let height = u32::from(u16::from_be_bytes([b[i + 3], b[i + 4]]));
let width = u32::from(u16::from_be_bytes([b[i + 5], b[i + 6]]));
return Ok((width, height));
}
i += length;
}
}
fn webp(b: &[u8]) -> Result<(u32, u32), Fault> {
if b.len() < 16 {
return Err(short("width", "width"));
}
match &b[12..16] {
b"VP8 " => {
if b.len() >= 26 && b[23..26] != [0x9D, 0x01, 0x2A] {
return Err(("header", "the lossy frame does not start with the code 9d 01 2a".to_owned()));
}
if b.len() < 28 {
return Err(short("width", "width"));
}
if b.len() < 30 {
return Err(short("height", "height"));
}
Ok((le16(&b[26..28]) & 0x3FFF, le16(&b[28..30]) & 0x3FFF))
}
b"VP8L" => {
if b.len() >= 21 && b[20] != 0x2F {
return Err(("header", format!("the lossless stream starts with {:02x}, not 2f", b[20])));
}
if b.len() < 25 {
return Err(short("width", "width"));
}
let bits = u32::from_le_bytes([b[21], b[22], b[23], b[24]]);
Ok((1 + (bits & 0x3FFF), 1 + ((bits >> 14) & 0x3FFF)))
}
b"VP8X" => {
if b.len() < 27 {
return Err(short("width", "width"));
}
if b.len() < 30 {
return Err(short("height", "height"));
}
let le24 = |s: &[u8]| u32::from(s[0]) | u32::from(s[1]) << 8 | u32::from(s[2]) << 16;
Ok((1 + le24(&b[24..27]), 1 + le24(&b[27..30])))
}
other => Err(("header", format!("the first chunk is `{}`, not VP8, VP8L or VP8X", String::from_utf8_lossy(other)))),
}
}