rahmen_exiv2/lib.rs
1//! Minimal safe wrapper over libexiv2.
2//!
3//! A thin C++ shim (see `shim.{h,cc}`) bound through the `cxx` crate. Only the
4//! read-only operations rahmen actually uses are exposed: loading an image and
5//! reading a single tag as an interpreted string. The unavoidable FFI `unsafe`
6//! is confined to this crate so the rahmen crate can keep `forbid(unsafe_code)`.
7
8use std::error::Error;
9use std::ffi::OsStr;
10use std::fmt;
11
12use cxx::UniquePtr;
13
14// SAFETY: this is the cxx FFI bridge. The `unsafe extern "C++"` block only
15// declares the signatures of the C++ functions in `shim.cc`; cxx generates the
16// marshalling glue and verifies the C++ side matches at compile time. The
17// declared functions are memory-safe: they take borrowed strings/handles and
18// return owned values, and all C++ exceptions are caught and surfaced as Err.
19#[cxx::bridge(namespace = "rahmen_exiv2")]
20mod ffi {
21 unsafe extern "C++" {
22 include!("src/shim.h");
23
24 /// Loaded exiv2 image with its metadata read.
25 type Image;
26
27 /// Open `path` and read its metadata.
28 fn open_image(path: &str) -> Result<UniquePtr<Image>>;
29
30 /// Interpreted (human-readable) string for `key`.
31 fn tag_interpreted(image: &Image, key: &str) -> Result<String>;
32 }
33}
34
35/// Error originating from the exiv2 wrapper.
36#[derive(Debug)]
37pub struct Exiv2Error(String);
38
39impl fmt::Display for Exiv2Error {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 write!(f, "exiv2 error: {}", self.0)
42 }
43}
44
45impl Error for Exiv2Error {}
46
47/// Image metadata loaded from a file.
48pub struct Metadata {
49 image: UniquePtr<ffi::Image>,
50}
51
52impl Metadata {
53 /// Load metadata from the file at `path`.
54 pub fn new_from_path<P: AsRef<OsStr>>(path: P) -> Result<Self, Exiv2Error> {
55 // exiv2 takes a UTF-8 path; reject non-UTF-8 rather than lose bytes.
56 let path = path
57 .as_ref()
58 .to_str()
59 .ok_or_else(|| Exiv2Error("path is not valid UTF-8".to_string()))?;
60 let image = ffi::open_image(path).map_err(|e| Exiv2Error(e.what().to_string()))?;
61 Ok(Self { image })
62 }
63
64 /// Read `tag` as an interpreted string. Returns an error when the tag is
65 /// absent or the key is invalid.
66 pub fn get_tag_interpreted_string(&self, tag: &str) -> Result<String, Exiv2Error> {
67 ffi::tag_interpreted(&self.image, tag).map_err(|e| Exiv2Error(e.what().to_string()))
68 }
69}