use crate::core::error::{XmpError, XmpResult};
use crate::core::metadata::XmpMeta;
use crate::files::handler::{FileHandler, XmpOptions};
use crate::files::registry::default_registry;
use std::io::{Cursor, Read, Seek, Write};
pub struct XmpFile {
meta: Option<XmpMeta>,
#[cfg(not(target_arch = "wasm32"))]
file_path: Option<std::path::PathBuf>,
#[allow(dead_code)] file_data: Option<Vec<u8>>,
#[allow(dead_code)] handler: Option<crate::files::registry::Handler>,
#[allow(dead_code)] options: XmpOptions,
is_open: bool,
}
impl XmpFile {
pub fn new() -> Self {
Self {
meta: None,
#[cfg(not(target_arch = "wasm32"))]
file_path: None,
file_data: None,
handler: None,
options: XmpOptions::default(),
is_open: false,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn open_with<P: AsRef<std::path::Path>>(
&mut self,
path: P,
options: XmpOptions,
) -> XmpResult<()> {
use std::fs;
let path = path.as_ref();
if options.use_packet_scanning && options.limited_scanning {
let file_ext = path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
const KNOWN_SCANNED_FILES: &[&str] = &["txt", "xml", "html", "htm"];
if !KNOWN_SCANNED_FILES.contains(&file_ext.as_str()) {
return Err(XmpError::NotSupported(format!(
"File type '{}' not in limited scanning list",
file_ext
)));
}
}
let file = fs::File::open(path)?;
self.file_path = Some(path.to_path_buf());
self.from_reader_with(file, options)
}
pub fn scan_for_xmp_packet(file_data: &[u8]) -> XmpResult<Option<XmpMeta>> {
let xpacket_start = b"<?xpacket";
let mut search_pos = 0;
while search_pos + xpacket_start.len() <= file_data.len() {
let Some(pos) = file_data[search_pos..]
.windows(xpacket_start.len())
.position(|window| window == xpacket_start)
else {
break;
};
let start_pos = search_pos + pos;
let xpacket_end_marker = b"<?xpacket end";
let Some(packet_end_offset) = file_data[start_pos..]
.windows(xpacket_end_marker.len())
.position(|window| window.starts_with(xpacket_end_marker))
else {
search_pos = start_pos + 1;
continue;
};
let end_marker_start = start_pos + packet_end_offset;
let Some(close_pos) = file_data[end_marker_start..]
.iter()
.enumerate()
.find(|(_, &b)| b == b'?')
.and_then(|(q_pos, _)| {
if end_marker_start + q_pos + 1 < file_data.len()
&& file_data[end_marker_start + q_pos + 1] == b'>'
{
let before_close = &file_data[end_marker_start..end_marker_start + q_pos];
if before_close.ends_with(b"\"w\"") || before_close.ends_with(b"\"r\"") {
Some(q_pos + 2)
} else {
None
}
} else {
None
}
})
else {
search_pos = start_pos + 1;
continue;
};
let packet_end_pos = end_marker_start + close_pos;
if let Ok(packet_str) = std::str::from_utf8(&file_data[start_pos..packet_end_pos]) {
match XmpMeta::parse(packet_str) {
Ok(meta) => return Ok(Some(meta)),
Err(_) => {
search_pos = start_pos + 1;
continue;
}
}
}
search_pos = start_pos + 1;
}
Ok(None)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn open<P: AsRef<std::path::Path>>(&mut self, path: P) -> XmpResult<()> {
use std::fs::File;
let file = File::open(path)?;
self.from_reader(file)
}
pub fn from_bytes(&mut self, data: &[u8]) -> XmpResult<()> {
self.from_bytes_with(data, XmpOptions::default())
}
pub fn from_bytes_with(&mut self, data: &[u8], options: XmpOptions) -> XmpResult<()> {
let cursor = Cursor::new(data);
self.from_reader_with(cursor, options)
}
pub fn from_reader<R: Read + Seek>(&mut self, reader: R) -> XmpResult<()> {
self.from_reader_with(reader, XmpOptions::default())
}
pub fn from_reader_with<R: Read + Seek>(
&mut self,
mut reader: R,
options: XmpOptions,
) -> XmpResult<()> {
self.meta = None;
#[cfg(not(target_arch = "wasm32"))]
{
self.handler = None;
self.is_open = false;
}
self.options = options;
self.file_data = None;
if options.use_packet_scanning {
let mut file_data = Vec::new();
reader.read_to_end(&mut file_data)?;
self.meta = Self::scan_for_xmp_packet(&file_data)?;
if options.for_update {
self.file_data = Some(file_data);
}
#[cfg(not(target_arch = "wasm32"))]
{
self.is_open = true;
}
return Ok(());
}
let registry = default_registry();
let handler = registry.find_by_detection(&mut reader)?;
if options.use_smart_handler {
let handler = handler.ok_or_else(|| {
XmpError::NotSupported("No smart file handler available to handle file".to_string())
})?;
if options.for_update {
reader.rewind()?;
let mut file_data = Vec::new();
reader.read_to_end(&mut file_data)?;
self.file_data = Some(file_data.clone());
let mut reader_cursor = Cursor::new(&file_data);
self.meta = handler.read_xmp(&mut reader_cursor, &options)?;
} else {
reader.rewind()?;
self.meta = handler.read_xmp(&mut reader, &options)?;
}
#[cfg(not(target_arch = "wasm32"))]
{
self.handler = Some(handler.clone());
self.is_open = true;
}
return Ok(());
}
if options.strict {
let handler = handler.ok_or_else(|| {
XmpError::NotSupported("No handler available for file format".to_string())
})?;
if options.for_update {
reader.rewind()?;
let mut file_data = Vec::new();
reader.read_to_end(&mut file_data)?;
self.file_data = Some(file_data.clone());
let mut reader_cursor = Cursor::new(&file_data);
self.meta = handler.read_xmp(&mut reader_cursor, &options)?;
} else {
reader.rewind()?;
self.meta = handler.read_xmp(&mut reader, &options)?;
}
#[cfg(not(target_arch = "wasm32"))]
{
self.handler = Some(handler.clone());
self.is_open = true;
}
return Ok(());
}
if let Some(handler) = handler {
if options.for_update {
reader.rewind()?;
let mut file_data = Vec::new();
reader.read_to_end(&mut file_data)?;
self.file_data = Some(file_data.clone());
let mut reader_cursor = Cursor::new(&file_data);
self.meta = handler.read_xmp(&mut reader_cursor, &options)?;
} else {
reader.rewind()?;
self.meta = handler.read_xmp(&mut reader, &options)?;
}
#[cfg(not(target_arch = "wasm32"))]
{
self.handler = Some(handler.clone());
self.is_open = true;
}
Ok(())
} else {
reader.rewind()?;
let mut file_data = Vec::new();
reader.read_to_end(&mut file_data)?;
self.meta = Self::scan_for_xmp_packet(&file_data)?;
if options.for_update {
self.file_data = Some(file_data);
}
#[cfg(not(target_arch = "wasm32"))]
{
self.is_open = true;
}
Ok(())
}
}
pub fn get_xmp(&self) -> Option<&XmpMeta> {
self.meta.as_ref()
}
pub fn get_xmp_mut(&mut self) -> Option<&mut XmpMeta> {
self.meta.as_mut()
}
pub fn put_xmp(&mut self, meta: XmpMeta) {
self.meta = Some(meta);
}
pub fn close(&mut self) {
let _ = self.try_close();
}
pub fn try_close(&mut self) -> XmpResult<()> {
if !self.is_open {
return Ok(());
}
#[cfg(not(target_arch = "wasm32"))]
{
if self.options.for_update {
if let Some(ref path) = self.file_path {
if let Some(ref meta) = self.meta {
use std::fs::File;
use std::io::BufWriter;
let handler = if let Some(ref h) = self.handler {
h.clone()
} else {
let registry = default_registry();
let mut reader =
Cursor::new(self.file_data.as_ref().ok_or_else(|| {
XmpError::BadValue(
"File data not available for handler detection. \
This can happen if the file was opened in read-only mode. \
Use XmpOptions::for_update() to enable writing."
.to_string(),
)
})?);
registry
.find_by_detection(&mut reader)?
.ok_or_else(|| {
XmpError::NotSupported(
"Unsupported file format for writing".to_string(),
)
})?
.clone()
};
let file_data = self
.file_data
.as_ref()
.ok_or_else(|| {
XmpError::BadValue(
"File data not available for writing. \
This can happen if the file was opened in read-only mode. \
Use XmpOptions::for_update() to enable writing."
.to_string(),
)
})?
.clone();
let mut reader = Cursor::new(&file_data);
let mut writer = BufWriter::new(File::create(path)?);
handler.write_xmp(&mut reader, &mut writer, meta)?;
writer.flush()?;
}
}
}
}
#[cfg(target_arch = "wasm32")]
{
}
self.is_open = false;
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> XmpResult<()> {
use std::fs::File;
let file = File::create(path)?;
self.write_to_writer(file)
}
pub fn write_to_bytes(&self) -> XmpResult<Vec<u8>> {
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
self.write_to_writer(cursor)?;
Ok(buffer)
}
pub fn write_to_writer<W: Write + Seek>(&self, mut writer: W) -> XmpResult<()> {
let meta = self.meta.as_ref().ok_or_else(|| {
XmpError::BadValue("No XMP metadata available for writing".to_string())
})?;
let file_data = self.file_data.as_ref().ok_or_else(|| {
XmpError::BadValue(
"Original file data not available for writing. \
To write XMP metadata, open the file with XmpOptions::for_update()."
.to_string(),
)
})?;
let registry = default_registry();
let mut reader = Cursor::new(file_data);
let handler = registry.find_by_detection(&mut reader)?.ok_or_else(|| {
XmpError::NotSupported("Unsupported file format for writing".to_string())
})?;
reader.set_position(0);
handler.write_xmp(&mut reader, &mut writer, meta)?;
writer.flush()?;
Ok(())
}
}
impl Default for XmpFile {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let file = XmpFile::new();
assert!(file.get_xmp().is_none());
}
#[test]
fn test_from_bytes_empty() {
let mut file = XmpFile::new();
let result = file.from_bytes(&[]);
assert!(result.is_err() || file.get_xmp().is_none());
}
#[test]
fn test_put_and_get_xmp() {
let mut file = XmpFile::new();
let meta = XmpMeta::new();
file.put_xmp(meta);
assert!(file.get_xmp().is_some());
}
}