use cuttlefish_abi::MediaKind;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
enum Source {
File(File),
Memory(Vec<u8>),
}
struct OpenFile {
source: Source,
len: u64,
kind: MediaKind,
}
#[derive(Default)]
pub struct Handles {
next: u32,
open: HashMap<u32, OpenFile>,
}
#[derive(Debug, thiserror::Error)]
pub enum HandleError {
#[error("no such handle: {0}")]
BadHandle(u32),
#[error("offset {offset} is past end of file ({len} bytes)")]
OffsetPastEnd {
offset: u64,
len: u64,
},
#[error("window of {0} bytes is too small to hold one character")]
WindowTooSmall(u64),
#[error(transparent)]
Io(#[from] std::io::Error),
}
pub struct Window {
pub text: String,
pub next_offset: u64,
}
impl Handles {
pub fn open(&mut self, path: &Path) -> Result<(u32, u64, MediaKind), HandleError> {
let mut file = File::open(path)?;
let len = file.metadata()?.len();
let mut head = vec![0u8; 4096.min(len as usize)];
file.read_exact(&mut head)?;
file.seek(SeekFrom::Start(0))?;
let kind = classify(&head, len);
let handle = self.next;
self.next += 1;
self.open.insert(
handle,
OpenFile {
source: Source::File(file),
len,
kind: kind.clone(),
},
);
Ok((handle, len, kind))
}
pub fn insert_bytes(&mut self, bytes: Vec<u8>, kind: MediaKind) -> (u32, u64) {
let len = bytes.len() as u64;
let handle = self.next;
self.next += 1;
self.open.insert(
handle,
OpenFile {
source: Source::Memory(bytes),
len,
kind,
},
);
(handle, len)
}
pub fn kind(&self, handle: u32) -> Result<MediaKind, HandleError> {
self.open
.get(&handle)
.map(|f| f.kind.clone())
.ok_or(HandleError::BadHandle(handle))
}
pub fn read_all(&mut self, handle: u32) -> Result<Vec<u8>, HandleError> {
let f = self
.open
.get_mut(&handle)
.ok_or(HandleError::BadHandle(handle))?;
match &mut f.source {
Source::Memory(bytes) => Ok(bytes.clone()),
Source::File(file) => {
let mut buf = Vec::with_capacity(f.len as usize);
file.seek(SeekFrom::Start(0))?;
file.read_to_end(&mut buf)?;
Ok(buf)
}
}
}
pub fn slice_bytes(
&mut self,
handle: u32,
offset: u64,
len: u64,
) -> Result<(Vec<u8>, u64), HandleError> {
let f = self
.open
.get_mut(&handle)
.ok_or(HandleError::BadHandle(handle))?;
if offset > f.len {
return Err(HandleError::OffsetPastEnd { offset, len: f.len });
}
let want = len.min(f.len - offset) as usize;
let mut buf = vec![0u8; want];
match &mut f.source {
Source::Memory(bytes) => {
buf.copy_from_slice(&bytes[offset as usize..offset as usize + want]);
}
Source::File(file) => {
file.seek(SeekFrom::Start(offset))?;
file.read_exact(&mut buf)?;
}
}
Ok((buf, offset + want as u64))
}
pub fn slice(&mut self, handle: u32, offset: u64, len: u64) -> Result<Window, HandleError> {
let f = self
.open
.get_mut(&handle)
.ok_or(HandleError::BadHandle(handle))?;
if offset > f.len {
return Err(HandleError::OffsetPastEnd { offset, len: f.len });
}
let want = len.min(f.len - offset) as usize;
let mut buf = vec![0u8; want];
match &mut f.source {
Source::Memory(bytes) => {
buf.copy_from_slice(&bytes[offset as usize..offset as usize + want]);
}
Source::File(file) => {
file.seek(SeekFrom::Start(offset))?;
file.read_exact(&mut buf)?;
}
}
let valid = match std::str::from_utf8(&buf) {
Ok(_) => buf.len(),
Err(e) => e.valid_up_to(),
};
if valid == 0 && !buf.is_empty() {
return Err(HandleError::WindowTooSmall(len));
}
buf.truncate(valid);
Ok(Window {
text: String::from_utf8(buf).expect("truncated at a validated boundary"),
next_offset: offset + valid as u64,
})
}
}
fn classify(head: &[u8], len: u64) -> MediaKind {
if head.starts_with(b"%PDF-") {
return MediaKind::Document {
pages: 0,
has_text_layer: false,
};
}
if head.starts_with(&[0x89, b'P', b'N', b'G']) {
return MediaKind::Image {
format: "png".into(),
};
}
if head.starts_with(&[0xFF, 0xD8, 0xFF]) {
return MediaKind::Image {
format: "jpeg".into(),
};
}
if head.starts_with(b"GIF8") {
return MediaKind::Image {
format: "gif".into(),
};
}
if head.len() >= 12 && head.starts_with(b"RIFF") && &head[8..12] == b"WEBP" {
return MediaKind::Image {
format: "webp".into(),
};
}
if len == 0 {
return MediaKind::Text;
}
match std::str::from_utf8(head) {
Ok(_) => MediaKind::Text,
Err(e) if e.error_len().is_none() && e.valid_up_to() > 0 => MediaKind::Text,
Err(_) => MediaKind::Binary,
}
}