Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions pixel_bridge/pixel_bridge.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "pixel_bridge.h"

#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
Expand Down Expand Up @@ -357,5 +358,54 @@ absl::StatusOr<ImageDecoder> ImageReader::IntoDecoder() && {
}
return ImageDecoder(std::move(decoder).value());
}
absl::StatusOr<std::vector<std::pair<std::string, std::string>>>
ImageReader::ExtractPngTextMetadata(absl::string_view png_string) {
absl::Span<const uint8_t> span(
reinterpret_cast<const uint8_t*>(png_string.data()), png_string.size());
rs_std::Result<rust::vec_u8::VecU8, rust::vec_u8::VecU8>
result = rust::reader::extract_png_text_metadata(span);
if (!result.has_value()) {
return absl::InvalidArgumentError(
StringViewFromVecU8(std::move(result).err()));
}
rust::vec_u8::VecU8 vec = std::move(result).value();
absl::string_view serialized = StringViewFromVecU8(vec);
std::vector<std::pair<std::string, std::string>> metadata;
size_t idx = 0;
auto decode_le32 = [](const char* data) -> uint32_t {
return static_cast<uint8_t>(data[0]) |
(static_cast<uint8_t>(data[1]) << 8) |
(static_cast<uint8_t>(data[2]) << 16) |
(static_cast<uint8_t>(data[3]) << 24);
};
while (serialized.size() - idx >= 4) {
uint32_t key_len = decode_le32(serialized.data() + idx);
idx += 4;
if (key_len > serialized.size() - idx) {
return absl::InvalidArgumentError("Truncated serialized metadata key");
}
std::string key(serialized.substr(idx, key_len));
idx += key_len;

if (serialized.size() - idx < 4) {
return absl::InvalidArgumentError(
"Missing serialized metadata value length");
}
uint32_t val_len = decode_le32(serialized.data() + idx);
idx += 4;
if (val_len > serialized.size() - idx) {
return absl::InvalidArgumentError("Truncated serialized metadata value");
}
std::string val(serialized.substr(idx, val_len));
idx += val_len;

metadata.push_back({std::move(key), std::move(val)});
}
if (idx < serialized.size()) {
return absl::InvalidArgumentError(
"Trailing unparsed bytes in serialized metadata");
}
return metadata;
}

} // namespace security::pixel_bridge
6 changes: 6 additions & 0 deletions pixel_bridge/pixel_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <variant>
#include <vector>

Expand Down Expand Up @@ -231,6 +232,11 @@ class ImageReader final {
// Guesses the format and constructs the correct decoder for the format.
absl::StatusOr<ImageDecoder> IntoDecoder() &&;

// Extracts all PNG text chunks (tEXt, zTXt, iTXt) as key-value pairs.
// Returns an error if parsing fails.
static absl::StatusOr<std::vector<std::pair<std::string, std::string>>>
ExtractPngTextMetadata(absl::string_view png_string);

private:
explicit ImageReader(rust::reader::ImageReader reader);
rust::reader::ImageReader reader_;
Expand Down
1 change: 1 addition & 0 deletions pixel_bridge/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ doctest = false
bytemuck = { version = "1", default-features = false, features = ["aarch64_simd", "derive", "extern_crate_alloc", "min_const_generics", "must_cast"] }
image = { version = "0.25", default-features = false, features = ["bmp", "exr", "gif", "hdr", "ico", "jpeg", "png", "tiff", "webp"] }
pic-scale-safe = "0.1"
png = { version = "0.18" }
tiff = { version = "0.11", features = ["deflate", "fax", "jpeg", "lzw"] }
89 changes: 89 additions & 0 deletions pixel_bridge/rust/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use image::{
codecs::png::PngDecoder as RustPngDecoder, codecs::tiff::TiffDecoder as RustTiffDecoder,
codecs::webp::WebPDecoder as RustWebPDecoder, ImageFormat, ImageReader as RustImageReader,
};
use png as png_crate;
use std::fs::File;
use std::io::{BufRead, BufReader, Cursor, Read, Seek};

Expand Down Expand Up @@ -200,3 +201,91 @@ fn ico(d: RustImageReader<Box<dyn ReadSeek>>) -> Result<ImageDecoder, String> {
RustIcoDecoder::new(d.into_inner()).map_err(|e| e.to_string())?,
))))
}

pub fn extract_png_text_metadata(
png_data: &[u8],
) -> Result<crate::vec_u8::VecU8, crate::vec_u8::VecU8> {
let decoder = png_crate::Decoder::new(std::io::Cursor::new(png_data));
let reader = decoder.read_info().map_err(|e| crate::vec_u8::VecU8::from(e.to_string()))?;
let info = reader.info();

let mut serialized = Vec::new();

let mut append_entry = |key: &str, text: &str| {
let key_bytes = key.as_bytes();
let val_bytes = text.as_bytes();
serialized.extend_from_slice(&(key_bytes.len() as u32).to_le_bytes());
serialized.extend_from_slice(key_bytes);
serialized.extend_from_slice(&(val_bytes.len() as u32).to_le_bytes());
serialized.extend_from_slice(val_bytes);
};

for text_chunk in &info.uncompressed_latin1_text {
append_entry(&text_chunk.keyword, &text_chunk.text);
}

for text_chunk in &info.compressed_latin1_text {
if let Ok(val_str) = text_chunk.get_text() {
append_entry(&text_chunk.keyword, &val_str);
}
}

for text_chunk in &info.utf8_text {
if let Ok(val_str) = text_chunk.get_text() {
append_entry(&text_chunk.keyword, &val_str);
}
}

Ok(crate::vec_u8::VecU8::from(serialized))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_extract_metadata_with_null_bytes() {
let mut buffer = Vec::new();
{
let mut encoder = png_crate::Encoder::new(&mut buffer, 1, 1);
encoder.set_color(png_crate::ColorType::Grayscale);
encoder.set_depth(png_crate::BitDepth::Eight);

// Add a text chunk with null bytes in the value
let key = "Owner";
let val = "G\0O\0O\0G\0L\0E";
encoder.add_text_chunk(key.to_string(), val.to_string()).unwrap();

let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&[0]).unwrap();
}

let result = extract_png_text_metadata(&buffer).unwrap();
let serialized = &*result;

// Under length-prefixed encoding (4-byte LE length), parse keys & values:
let mut idx = 0;
let mut parsed = Vec::new();
while idx + 4 <= serialized.len() {
let key_len = u32::from_le_bytes(serialized[idx..idx + 4].try_into().unwrap()) as usize;
idx += 4;
assert!(idx + key_len <= serialized.len());
let key = std::str::from_utf8(&serialized[idx..idx + key_len]).unwrap().to_string();
idx += key_len;

assert!(idx + 4 <= serialized.len());
let val_len = u32::from_le_bytes(serialized[idx..idx + 4].try_into().unwrap()) as usize;
idx += 4;
assert!(idx + val_len <= serialized.len());
let val = std::str::from_utf8(&serialized[idx..idx + val_len]).unwrap().to_string();
idx += val_len;

parsed.push((key, val));
}
assert_eq!(idx, serialized.len());

assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].0, "Owner");
assert_eq!(parsed[0].1, "G\0O\0O\0G\0L\0E");
}
}
7 changes: 7 additions & 0 deletions pixel_bridge/rust/vec_u8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ impl VecU8 {
}
}

impl std::ops::Deref for VecU8 {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl From<Vec<u8>> for VecU8 {
fn from(val: Vec<u8>) -> Self {
Self(val)
Expand Down
Loading