use std::{
fs,
io::{self, Read, SeekFrom},
path::Path,
str,
};
use byteorder::{ReadBytesExt, WriteBytesExt};
use crate::{
asset_io::{
AllowedExclusion, AssetBoxHash, AssetIO, AssetPatch, BoxMap, C2paReader, C2paWriter,
ComposedManifestRef, ExclusionKind, ObjectLocations, ObjectType, ReadSeek, ReadWriteSeek,
RemoteManifestUrl, WriteXmp, C2PA_BOXHASH,
},
error::Result,
utils::io_utils::stream_len,
Error,
};
const XMP_MAGIC_TRAILER_LEN: usize = 257;
pub struct GifIO {}
impl C2paReader for GifIO {
fn read_c2pa(&self, input_stream: &mut dyn ReadSeek) -> Result<Vec<u8>> {
self.find_c2pa_block(input_stream)?
.map(|marker| marker.block.data_sub_blocks.to_decoded_bytes())
.ok_or(Error::JumbfNotFound)
}
fn read_xmp(&self, input_stream: &mut dyn ReadSeek) -> Option<String> {
let mut bytes = self
.find_xmp_block(input_stream)
.ok()?
.map(|marker| marker.block.data_sub_blocks.to_decoded_bytes())?;
if bytes.len() < XMP_MAGIC_TRAILER_LEN {
return None;
}
if let Some(byte) = bytes.get(bytes.len() - XMP_MAGIC_TRAILER_LEN) {
if *byte != 1 {
return None;
}
}
for (i, byte) in bytes
.iter()
.rev()
.take(XMP_MAGIC_TRAILER_LEN - 1)
.enumerate()
{
if *byte != i as u8 {
return None;
}
}
bytes.truncate(bytes.len() - XMP_MAGIC_TRAILER_LEN);
String::from_utf8(bytes).ok()
}
}
impl C2paWriter for GifIO {
fn write_c2pa(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
store_bytes: &[u8],
) -> Result<()> {
let old_block_marker = self.find_c2pa_block(input_stream)?;
let new_block = ApplicationExtension::new_c2pa(store_bytes)?;
match old_block_marker {
Some(old_block_marker) => self.replace_block(
input_stream,
output_stream,
&old_block_marker.into(),
&new_block.into(),
),
None => self.insert_block(input_stream, output_stream, &new_block.into()),
}
}
fn get_object_locations(
&self,
input_stream: &mut dyn ReadSeek,
) -> Result<Vec<ObjectLocations>> {
let c2pa_block = self.find_c2pa_block(input_stream)?;
match c2pa_block {
Some(c2pa_block) => Ok(vec![
ObjectLocations {
offset: 0,
length: c2pa_block.start() - 1,
htype: ObjectType::Other,
},
ObjectLocations {
offset: c2pa_block.start(),
length: c2pa_block.len(),
htype: ObjectType::C2pa,
},
ObjectLocations {
offset: c2pa_block.end(),
length: stream_len(input_stream)? - c2pa_block.end(),
htype: ObjectType::Other,
},
]),
None => {
self.skip_preamble(input_stream)?;
let end_preamble_pos = input_stream.stream_position()?;
Ok(vec![
ObjectLocations {
offset: 0,
length: end_preamble_pos - 1,
htype: ObjectType::Other,
},
ObjectLocations {
offset: end_preamble_pos,
length: 1, htype: ObjectType::C2pa,
},
ObjectLocations {
offset: end_preamble_pos + 1,
length: stream_len(input_stream)? - end_preamble_pos,
htype: ObjectType::Other,
},
])
}
}
}
fn remove_c2pa(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
) -> Result<()> {
match self.find_c2pa_block(input_stream)? {
Some(block_marker) => {
self.remove_block(input_stream, output_stream, &block_marker.into())
}
None => {
input_stream.rewind()?;
io::copy(input_stream, output_stream)?;
Ok(())
}
}
}
}
impl AssetPatch for GifIO {
fn patch_c2pa_file(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()> {
let mut stream = fs::OpenOptions::new()
.read(true)
.write(true)
.open(asset_path)
.map_err(Error::IoError)?;
let old_block_marker = match self.find_c2pa_block(&mut stream)? {
Some(old_block_marker) => old_block_marker,
None => return Err(Error::JumbfNotFound),
};
let new_block = ApplicationExtension::new_c2pa(store_bytes)?;
self.replace_block_in_place(&mut stream, &old_block_marker.into(), &new_block.into())
}
}
impl WriteXmp for GifIO {
fn write_xmp(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
xmp: &str,
) -> Result<()> {
let old_block_marker = self.find_xmp_block(input_stream)?;
let new_block = ApplicationExtension::new_xmp(xmp.as_bytes().to_vec())?;
match old_block_marker {
Some(old_block_marker) => self.replace_block(
input_stream,
output_stream,
&old_block_marker.into(),
&new_block.into(),
),
None => self.insert_block(input_stream, output_stream, &new_block.into()),
}
}
}
impl ComposedManifestRef for GifIO {
fn compose_manifest(&self, manifest_data: &[u8], _format: &str) -> Result<Vec<u8>> {
ApplicationExtension::new_c2pa(manifest_data)?.to_bytes()
}
}
impl AssetBoxHash for GifIO {
fn get_box_map(&self, input_stream: &mut dyn ReadSeek) -> Result<Vec<BoxMap>> {
let c2pa_block_exists = self.find_c2pa_block(input_stream)?.is_some();
let mut box_maps = Vec::new();
let mut last_marker: Option<BlockMarker<Block>> = None;
for marker in Blocks::new(input_stream)? {
let marker = marker?;
if !c2pa_block_exists {
if let Some(last_marker) = last_marker.as_ref() {
let should_insert_placeholder = match last_marker.block {
Block::GlobalColorTable(_) => true,
Block::LogicalScreenDescriptor(_)
if !matches!(marker.block, Block::GlobalColorTable(_)) =>
{
true
}
_ => false,
};
if should_insert_placeholder {
box_maps.push(
BlockMarker {
block: Block::ApplicationExtension(ApplicationExtension::new_c2pa(
&[],
)?),
start: marker.start,
len: 0,
}
.to_box_map()?,
);
}
}
}
match marker.block {
Block::LocalColorTable(_) | Block::GlobalColorTable(_) => {
match box_maps.last_mut() {
Some(last_box_map) => {
last_box_map.range_len += marker.len();
}
None => return Err(Error::NotFound),
}
}
_ => {
box_maps.push(marker.to_box_map()?);
}
}
last_marker = Some(marker);
}
Ok(box_maps)
}
}
impl AssetIO for GifIO {
fn new(_asset_type: &str) -> Self
where
Self: Sized,
{
GifIO {}
}
fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
Box::new(GifIO::new(asset_type))
}
fn get_reader(&self) -> &dyn C2paReader {
self
}
fn get_writer(&self, asset_type: &str) -> Option<Box<dyn C2paWriter>> {
Some(Box::new(GifIO::new(asset_type)))
}
fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
Some(self)
}
fn remote_manifest_url_ref(&self) -> Option<&dyn RemoteManifestUrl> {
Some(self)
}
fn write_xmp_ref(&self) -> Option<&dyn WriteXmp> {
Some(self)
}
fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> {
Some(self)
}
fn asset_box_hash_ref(&self) -> Option<&dyn AssetBoxHash> {
Some(self)
}
fn supported_types(&self) -> &[&str] {
&["gif", "image/gif"]
}
}
impl GifIO {
fn skip_preamble(&self, stream: &mut dyn ReadSeek) -> Result<()> {
stream.rewind()?;
Header::from_stream(stream)?;
let logical_screen_descriptor = LogicalScreenDescriptor::from_stream(stream)?;
if logical_screen_descriptor.global_color_table_flag {
GlobalColorTable::from_stream(
stream,
logical_screen_descriptor.global_color_table_size,
)?;
}
Ok(())
}
fn find_c2pa_block(
&self,
stream: &mut dyn ReadSeek,
) -> Result<Option<BlockMarker<ApplicationExtension>>> {
self.find_app_block_from_iterator(
ApplicationExtensionKind::C2pa,
Blocks::new(stream)?.take_while(|marker| {
!matches!(
marker,
Ok(BlockMarker {
block: Block::ImageDescriptor(_),
..
})
)
}),
)
}
fn find_xmp_block(
&self,
stream: &mut dyn ReadSeek,
) -> Result<Option<BlockMarker<ApplicationExtension>>> {
self.find_app_block_from_iterator(ApplicationExtensionKind::Xmp, Blocks::new(stream)?)
}
fn find_app_block_from_iterator(
&self,
kind: ApplicationExtensionKind,
mut iterator: impl Iterator<Item = Result<BlockMarker<Block>>>,
) -> Result<Option<BlockMarker<ApplicationExtension>>> {
iterator
.find_map(|marker| match marker {
Ok(marker) => match marker.block {
Block::ApplicationExtension(app_ext) if app_ext.kind() == kind => {
Some(Ok(BlockMarker {
start: marker.start,
len: marker.len,
block: app_ext,
}))
}
_ => None,
},
Err(err) => Some(Err(err)),
})
.transpose()
}
fn remove_block(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
block_meta: &BlockMarker<Block>,
) -> Result<()> {
input_stream.rewind()?;
output_stream.rewind()?;
let mut start_stream = input_stream.take(block_meta.start());
io::copy(&mut start_stream, output_stream)?;
let input_stream = start_stream.into_inner();
input_stream.seek(SeekFrom::Current(i64::try_from(block_meta.len())?))?;
io::copy(input_stream, output_stream)?;
Ok(())
}
fn replace_block(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
old_block_marker: &BlockMarker<Block>,
new_block: &Block,
) -> Result<()> {
input_stream.rewind()?;
output_stream.rewind()?;
let mut start_stream = input_stream.take(old_block_marker.start());
io::copy(&mut start_stream, output_stream)?;
output_stream.write_all(&new_block.to_bytes()?)?;
let input_stream = start_stream.into_inner();
input_stream.seek(SeekFrom::Current(i64::try_from(old_block_marker.len())?))?;
io::copy(input_stream, output_stream)?;
Ok(())
}
#[allow(dead_code)] fn replace_block_in_place(
&self,
stream: &mut dyn ReadWriteSeek,
old_block_marker: &BlockMarker<Block>,
new_block: &Block,
) -> Result<()> {
let new_bytes = new_block.to_bytes()?;
if new_bytes.len() as u64 != old_block_marker.len() {
return Err(Error::EmbeddingError);
}
stream.seek(SeekFrom::Start(old_block_marker.start()))?;
stream.write_all(&new_bytes)?;
Ok(())
}
fn insert_block(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
block: &Block,
) -> Result<()> {
self.skip_preamble(input_stream)?;
let end_preamble_pos = input_stream.stream_position()?;
input_stream.rewind()?;
output_stream.rewind()?;
let mut start_stream = input_stream.take(end_preamble_pos);
io::copy(&mut start_stream, output_stream)?;
output_stream.write_all(&block.to_bytes()?)?;
let input_stream = start_stream.into_inner();
io::copy(input_stream, output_stream)?;
self.update_to_89a(output_stream)
}
fn update_to_89a(&self, stream: &mut dyn ReadWriteSeek) -> Result<()> {
stream.seek(SeekFrom::Start(4))?;
stream.write_u8(0x39)?;
Ok(())
}
}
struct Blocks<'a> {
next: Option<BlockMarker<Block>>,
stream: &'a mut dyn ReadSeek,
reached_trailer: bool,
}
impl<'a> Blocks<'a> {
fn new(stream: &'a mut dyn ReadSeek) -> Result<Blocks<'a>> {
stream.rewind()?;
let start = stream.stream_position()?;
let block = Block::Header(Header::from_stream(stream)?);
let end = stream.stream_position()?;
Ok(Blocks {
next: Some(BlockMarker {
len: end - start,
start,
block,
}),
stream,
reached_trailer: false,
})
}
fn parse_next(&mut self) -> Result<BlockMarker<Block>> {
match self.next.take() {
Some(marker) => {
self.next = marker.block.next_block_hint(self.stream)?;
Ok(marker)
}
None => {
let marker = Block::from_stream(self.stream)?;
self.next = marker.block.next_block_hint(self.stream)?;
if let Block::Trailer = marker.block {
self.reached_trailer = true;
}
Ok(marker)
}
}
}
}
impl Iterator for Blocks<'_> {
type Item = Result<BlockMarker<Block>>;
fn next(&mut self) -> Option<Self::Item> {
match self.reached_trailer {
true => None,
false => match self.parse_next() {
Ok(marker) => Some(Ok(marker)),
Err(err) => Some(Err(err)),
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct BlockMarker<T> {
start: u64,
len: u64,
block: T,
}
impl<T> BlockMarker<T> {
fn len(&self) -> u64 {
self.len
}
fn start(&self) -> u64 {
self.start
}
fn end(&self) -> u64 {
self.start + self.len
}
}
impl BlockMarker<Block> {
fn to_box_map(&self) -> Result<BoxMap> {
let mut names = Vec::new();
if let Some(name) = self.block.box_id() {
names.push(name.to_owned());
}
Ok(BoxMap::new(names, self.start(), self.len())
.with_allowed_exclusions(self.block.allowed_exclusions(self.len())))
}
}
impl From<BlockMarker<ApplicationExtension>> for BlockMarker<Block> {
fn from(value: BlockMarker<ApplicationExtension>) -> Self {
BlockMarker {
start: value.start,
len: value.len,
block: Block::ApplicationExtension(value.block),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum Block {
Header(Header),
LogicalScreenDescriptor(LogicalScreenDescriptor),
GlobalColorTable(GlobalColorTable),
GraphicControlExtension(GraphicControlExtension),
PlainTextExtension(PlainTextExtension),
ApplicationExtension(ApplicationExtension),
CommentExtension(CommentExtension),
ImageDescriptor(ImageDescriptor),
LocalColorTable(LocalColorTable),
ImageData(ImageData),
Trailer,
}
impl Block {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<BlockMarker<Block>> {
let start = stream.stream_position()?;
let ext_introducer = stream.read_u8()?;
let block = match ext_introducer {
0x21 => {
let ext_label = stream.read_u8()?;
match ext_label {
0xff => Ok(Block::ApplicationExtension(
ApplicationExtension::from_stream(stream)?,
)),
0xfe => Ok(Block::CommentExtension(CommentExtension::from_stream(
stream,
)?)),
0xf9 => Ok(Block::GraphicControlExtension(
GraphicControlExtension::from_stream(stream)?,
)),
0x01 => Ok(Block::PlainTextExtension(PlainTextExtension::from_stream(
stream,
)?)),
ext_label => Err(Error::InvalidAsset(format!(
"Invalid block extension label: {ext_label}"
))),
}
}
0x2c => Ok(Block::ImageDescriptor(ImageDescriptor::from_stream(
stream,
)?)),
0x3b => Ok(Block::Trailer),
ext_introducer => Err(Error::InvalidAsset(format!(
"Invalid block id: {ext_introducer}"
))),
}?;
let end = stream.stream_position()?;
Ok(BlockMarker {
start,
len: end - start,
block,
})
}
fn next_block_hint(&self, stream: &mut dyn ReadSeek) -> Result<Option<BlockMarker<Block>>> {
let start = stream.stream_position()?;
let next_block = match self {
Block::Header(_) => Some(Block::LogicalScreenDescriptor(
LogicalScreenDescriptor::from_stream(stream)?,
)),
Block::LogicalScreenDescriptor(logical_screen_descriptor) => {
match logical_screen_descriptor.global_color_table_flag {
true => Some(Block::GlobalColorTable(GlobalColorTable::from_stream(
stream,
logical_screen_descriptor.global_color_table_size,
)?)),
false => None,
}
}
Block::GlobalColorTable(_) => None,
Block::GraphicControlExtension(_) => None,
Block::PlainTextExtension(_) => None,
Block::ApplicationExtension(_) => None,
Block::CommentExtension(_) => None,
Block::ImageDescriptor(image_descriptor) => {
match image_descriptor.local_color_table_flag {
true => Some(Block::LocalColorTable(LocalColorTable::from_stream(
stream,
image_descriptor.local_color_table_size,
)?)),
false => Some(Block::ImageData(ImageData::from_stream(stream)?)),
}
}
Block::LocalColorTable(_) => Some(Block::ImageData(ImageData::from_stream(stream)?)),
Block::ImageData(_) => None,
Block::Trailer => None,
};
let end = stream.stream_position()?;
Ok(next_block.map(|block| BlockMarker {
len: end - start,
start,
block,
}))
}
fn box_id(&self) -> Option<&'static str> {
match self {
Block::Header(_) => Some("GIF89a"),
Block::LogicalScreenDescriptor(_) => Some("LSD"),
Block::GlobalColorTable(_) => None,
Block::GraphicControlExtension(_) => Some("21F9"),
Block::PlainTextExtension(_) => Some("2101"),
Block::ApplicationExtension(application_extension) => {
match ApplicationExtensionKind::C2pa == application_extension.kind() {
true => Some(C2PA_BOXHASH),
false => Some("21FF"),
}
}
Block::CommentExtension(_) => Some("21FE"),
Block::ImageDescriptor(_) => Some("2C"),
Block::LocalColorTable(_) => None,
Block::ImageData(_) => Some("TBID"),
Block::Trailer => Some("3B"),
}
}
fn allowed_exclusions(&self, box_len: u64) -> Vec<AllowedExclusion> {
match self {
Block::ApplicationExtension(application_extension)
if ApplicationExtensionKind::C2pa == application_extension.kind() =>
{
vec![AllowedExclusion::whole_box(box_len)]
}
Block::ApplicationExtension(application_extension)
if ApplicationExtensionKind::Xmp == application_extension.kind() =>
{
metadata_exclusions_from_sub_blocks(
&application_extension.data_sub_blocks.bytes,
14,
)
}
Block::CommentExtension(comment) => {
metadata_exclusions_from_sub_blocks(&comment.data_sub_blocks.bytes, 2)
}
_ => Vec::new(),
}
}
fn to_bytes(&self) -> Result<Vec<u8>> {
match self {
Block::ApplicationExtension(app_ext) => app_ext.to_bytes(),
_ => Err(Error::UnsupportedType),
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct Header {
}
impl Header {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<Header> {
let mut signature = [0u8; 3];
stream.read_exact(&mut signature)?;
if signature != *b"GIF" {
return Err(Error::InvalidAsset(format!(
"invalid header signature: expected \"GIF\", found \"{}\"",
String::from_utf8_lossy(&signature)
)));
}
let mut version = [0u8; 3];
stream.read_exact(&mut version)?;
if version != *b"87a" && version != *b"89a" {
return Err(Error::InvalidAsset(format!(
"invalid header version: expected \"89a\" or \"87a\", found \"{}\"",
String::from_utf8_lossy(&version)
)));
}
Ok(Header {
})
}
}
#[derive(Debug, Clone, PartialEq)]
struct LogicalScreenDescriptor {
global_color_table_flag: bool,
global_color_table_size: u8,
}
impl LogicalScreenDescriptor {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<LogicalScreenDescriptor> {
stream.seek(SeekFrom::Current(4))?;
let packed = stream.read_u8()?;
let global_color_table_flag = (packed >> 7) & 1;
let global_color_table_size = packed & 0b111;
stream.seek(SeekFrom::Current(2))?;
Ok(LogicalScreenDescriptor {
global_color_table_flag: global_color_table_flag != 0,
global_color_table_size,
})
}
}
#[derive(Debug, Clone, PartialEq)]
struct GlobalColorTable {}
impl GlobalColorTable {
fn from_stream(stream: &mut dyn ReadSeek, size: u8) -> Result<GlobalColorTable> {
stream.seek(SeekFrom::Current(3 * (2_i64.pow(size as u32 + 1))))?;
Ok(GlobalColorTable {})
}
}
#[derive(Debug, PartialEq)]
enum ApplicationExtensionKind {
C2pa,
Xmp,
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
struct ApplicationExtension {
identifier: [u8; 8],
authentication_code: [u8; 3],
data_sub_blocks: DataSubBlocks,
}
impl ApplicationExtension {
fn new_c2pa(bytes: &[u8]) -> Result<ApplicationExtension> {
Ok(ApplicationExtension {
identifier: *b"C2PA_GIF",
authentication_code: [0x01, 0x00, 0x00],
data_sub_blocks: DataSubBlocks::from_decoded_bytes(bytes)?,
})
}
fn new_xmp(mut bytes: Vec<u8>) -> Result<ApplicationExtension> {
bytes.reserve(XMP_MAGIC_TRAILER_LEN);
bytes.push(1);
for byte in (0..=255).rev() {
bytes.push(byte);
}
Ok(ApplicationExtension {
identifier: *b"XMP Data",
authentication_code: [0x58, 0x4d, 0x50],
data_sub_blocks: DataSubBlocks::from_decoded_bytes(&bytes)?,
})
}
fn from_stream(stream: &mut dyn ReadSeek) -> Result<ApplicationExtension> {
let app_block_size = stream.read_u8()?;
if app_block_size != 0x0b {
return Err(Error::InvalidAsset(format!(
"Invalid block size for app block extension {app_block_size}!=11"
)));
}
let mut app_id = [0u8; 8];
stream.read_exact(&mut app_id)?;
let mut app_auth_code = [0u8; 3];
stream.read_exact(&mut app_auth_code)?;
let mut app_block_ext = ApplicationExtension {
identifier: app_id,
authentication_code: app_auth_code,
data_sub_blocks: DataSubBlocks::empty(),
};
match app_block_ext.kind() {
ApplicationExtensionKind::C2pa | ApplicationExtensionKind::Xmp => {
app_block_ext.data_sub_blocks = DataSubBlocks::from_encoded_stream(stream)?;
}
ApplicationExtensionKind::Unknown => {
DataSubBlocks::from_encoded_stream_and_skip(stream)?;
}
};
Ok(app_block_ext)
}
fn kind(&self) -> ApplicationExtensionKind {
match (&self.identifier, self.authentication_code) {
(b"C2PA_GIF", [0x01, 0x00, 0x00]) => ApplicationExtensionKind::C2pa,
(b"XMP Data", [0x58, 0x4d, 0x50]) => ApplicationExtensionKind::Xmp,
(_, _) => ApplicationExtensionKind::Unknown,
}
}
fn to_bytes(&self) -> Result<Vec<u8>> {
let bytes = self.data_sub_blocks.to_encoded_bytes();
let mut header = Vec::with_capacity(14 + bytes.len().div_ceil(255) + bytes.len() + 1);
header.push(0x21);
header.push(0xff);
header.push(0x0b);
header.extend_from_slice(&self.identifier);
header.extend_from_slice(&self.authentication_code);
header.extend_from_slice(bytes);
Ok(header)
}
}
impl From<ApplicationExtension> for Block {
fn from(value: ApplicationExtension) -> Self {
Block::ApplicationExtension(value)
}
}
#[derive(Debug, Clone, PartialEq)]
struct PlainTextExtension {}
impl PlainTextExtension {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<PlainTextExtension> {
stream.seek(SeekFrom::Current(11))?;
DataSubBlocks::from_encoded_stream_and_skip(stream)?;
Ok(PlainTextExtension {})
}
}
#[derive(Debug, Clone, PartialEq)]
struct CommentExtension {
data_sub_blocks: DataSubBlocks,
}
impl CommentExtension {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<CommentExtension> {
Ok(CommentExtension {
data_sub_blocks: DataSubBlocks::from_encoded_stream(stream)?,
})
}
}
#[derive(Debug, Clone, PartialEq)]
struct GraphicControlExtension {}
impl GraphicControlExtension {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<GraphicControlExtension> {
stream.seek(SeekFrom::Current(6))?;
Ok(GraphicControlExtension {})
}
}
#[derive(Debug, Clone, PartialEq)]
struct ImageDescriptor {
local_color_table_flag: bool,
local_color_table_size: u8,
}
impl ImageDescriptor {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<ImageDescriptor> {
stream.seek(SeekFrom::Current(8))?;
let packed = stream.read_u8()?;
let local_color_table_flag = (packed >> 7) & 1;
let local_color_table_size = packed & 0b111;
Ok(ImageDescriptor {
local_color_table_flag: local_color_table_flag != 0,
local_color_table_size,
})
}
}
#[derive(Debug, Clone, PartialEq)]
struct LocalColorTable {}
impl LocalColorTable {
fn from_stream(
stream: &mut dyn ReadSeek,
local_color_table_size: u8,
) -> Result<LocalColorTable> {
stream.seek(SeekFrom::Current(
3 * (2_i64.pow(local_color_table_size as u32 + 1)),
))?;
Ok(LocalColorTable {})
}
}
#[derive(Debug, Clone, PartialEq)]
struct ImageData {}
impl ImageData {
fn from_stream(stream: &mut dyn ReadSeek) -> Result<ImageData> {
stream.seek(SeekFrom::Current(1))?;
DataSubBlocks::from_encoded_stream_and_skip(stream)?;
Ok(ImageData {})
}
}
#[derive(Debug, Clone, PartialEq)]
struct DataSubBlocks {
bytes: Vec<u8>,
}
impl DataSubBlocks {
fn empty() -> DataSubBlocks {
DataSubBlocks { bytes: vec![0] }
}
fn from_decoded_bytes(bytes: &[u8]) -> Result<DataSubBlocks> {
let mut data_sub_blocks = Vec::with_capacity(bytes.len().div_ceil(255) + bytes.len() + 1);
for chunk in bytes.chunks(255) {
data_sub_blocks.push(chunk.len() as u8);
data_sub_blocks.extend_from_slice(chunk);
}
data_sub_blocks.push(0);
Ok(DataSubBlocks {
bytes: data_sub_blocks,
})
}
fn from_encoded_stream(stream: &mut dyn ReadSeek) -> Result<DataSubBlocks> {
let mut data_sub_blocks = Vec::new();
loop {
let sub_block_size = stream.read_u8()?;
if sub_block_size == 0 {
break;
}
data_sub_blocks.push(sub_block_size);
let start = data_sub_blocks.len();
let end = start + sub_block_size as usize;
data_sub_blocks.resize(end, 0);
stream.read_exact(&mut data_sub_blocks[start..end])?;
}
data_sub_blocks.push(0);
Ok(DataSubBlocks {
bytes: data_sub_blocks,
})
}
fn from_encoded_stream_and_skip(stream: &mut dyn ReadSeek) -> Result<u64> {
let mut length = 0;
loop {
let sub_block_size = stream.read_u8()?;
length += 1;
if sub_block_size == 0 {
break;
} else {
length += sub_block_size as u64;
stream.seek(SeekFrom::Current(sub_block_size as i64))?;
}
}
Ok(length)
}
fn to_encoded_bytes(&self) -> &[u8] {
&self.bytes
}
fn to_decoded_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(gif_chunks(&self.bytes).map(|c| c.len()).sum());
for chunk in gif_chunks(&self.bytes) {
bytes.extend_from_slice(chunk);
}
bytes
}
}
fn gif_chunks(mut encoded_bytes: &[u8]) -> impl Iterator<Item = &[u8]> {
std::iter::from_fn(move || {
let (&len, rest) = encoded_bytes.split_first()?;
if len == 0 {
return None;
}
let (chunk, rest) = rest.split_at_checked(len.into())?;
encoded_bytes = rest;
Some(chunk)
})
}
fn sub_block_data_ranges(encoded_bytes: &[u8]) -> Vec<(u64, u64)> {
let mut ranges = Vec::new();
let mut pos: u64 = 0;
for chunk in gif_chunks(encoded_bytes) {
ranges.push((pos + 1, chunk.len() as u64));
pos += 1 + chunk.len() as u64;
}
ranges
}
fn metadata_exclusions_from_sub_blocks(
encoded_bytes: &[u8],
header_len: u64,
) -> Vec<AllowedExclusion> {
sub_block_data_ranges(encoded_bytes)
.into_iter()
.map(|(offset, length)| AllowedExclusion {
start: offset + header_len,
length,
kind: ExclusionKind::AssetMetadata,
})
.collect()
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use io::{Cursor, Seek};
use super::*;
use crate::utils::xmp_inmemory_utils::extract_provenance;
const SAMPLE1: &[u8] = include_bytes!("../../tests/fixtures/sample1.gif");
#[test]
fn test_allowed_exclusions() {
let comment = CommentExtension {
data_sub_blocks: DataSubBlocks {
bytes: vec![5, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0],
},
};
assert_eq!(
Block::CommentExtension(comment).allowed_exclusions(20),
vec![
AllowedExclusion {
start: 3,
length: 5,
kind: ExclusionKind::AssetMetadata,
},
AllowedExclusion {
start: 9,
length: 3,
kind: ExclusionKind::AssetMetadata,
},
]
);
assert_eq!(
Block::ApplicationExtension(ApplicationExtension::new_c2pa(&[]).unwrap())
.allowed_exclusions(20),
vec![AllowedExclusion {
start: 0,
length: 20,
kind: ExclusionKind::ManifestOrPadding,
}]
);
let xmp = ApplicationExtension::new_xmp(b"xmp data".to_vec()).unwrap();
assert_eq!(
Block::ApplicationExtension(xmp).allowed_exclusions(300),
vec![
AllowedExclusion {
start: 15,
length: 255,
kind: ExclusionKind::AssetMetadata,
},
AllowedExclusion {
start: 271,
length: 10,
kind: ExclusionKind::AssetMetadata,
},
]
);
let unknown = ApplicationExtension {
identifier: *b"UNKNOWN0",
authentication_code: [0, 0, 0],
data_sub_blocks: DataSubBlocks::empty(),
};
assert!(Block::ApplicationExtension(unknown)
.allowed_exclusions(20)
.is_empty());
assert!(Block::Header(Header {}).allowed_exclusions(20).is_empty());
assert!(Block::Trailer.allowed_exclusions(20).is_empty());
}
#[test]
fn test_read_blocks() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let blocks: Vec<_> = Blocks::new(&mut stream)?.collect::<Result<_>>()?;
assert_eq!(
blocks.first(),
Some(&BlockMarker {
start: 0,
len: 6,
block: Block::Header(Header {})
})
);
assert_eq!(
blocks.get(1),
Some(&BlockMarker {
start: 6,
len: 7,
block: Block::LogicalScreenDescriptor(LogicalScreenDescriptor {
global_color_table_flag: true,
global_color_table_size: 7
})
})
);
assert_eq!(
blocks.get(2),
Some(&BlockMarker {
start: 13,
len: 768,
block: Block::GlobalColorTable(GlobalColorTable {})
})
);
assert_eq!(
blocks.get(3),
Some(&BlockMarker {
start: 781,
len: 19,
block: Block::ApplicationExtension(ApplicationExtension {
identifier: *b"NETSCAPE",
authentication_code: [50, 46, 48],
data_sub_blocks: DataSubBlocks::empty(),
})
})
);
assert_eq!(
blocks.get(4),
Some(&BlockMarker {
start: 800,
len: 8,
block: Block::GraphicControlExtension(GraphicControlExtension {})
})
);
assert_eq!(
blocks.get(5),
Some(&BlockMarker {
start: 808,
len: 52,
block: Block::CommentExtension(CommentExtension {
data_sub_blocks: DataSubBlocks {
bytes: SAMPLE1[810..860].to_vec()
}
})
})
);
Ok(())
}
#[test]
fn test_write_remove_block() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let gif_io = GifIO {};
assert!(matches!(
gif_io.read_c2pa(&mut stream),
Err(Error::JumbfNotFound)
));
let mut output_stream1 = Cursor::new(Vec::with_capacity(SAMPLE1.len() + 15 + 7));
let random_bytes = [1, 2, 3, 4, 3, 2, 1];
gif_io.write_c2pa(&mut stream, &mut output_stream1, &random_bytes)?;
let data_written = gif_io.read_c2pa(&mut output_stream1)?;
assert_eq!(data_written, random_bytes);
let mut output_stream2 = Cursor::new(Vec::with_capacity(SAMPLE1.len()));
gif_io.remove_c2pa(&mut output_stream1, &mut output_stream2)?;
assert!(matches!(
gif_io.read_c2pa(&mut stream),
Err(Error::JumbfNotFound)
));
let mut bytes = Vec::new();
output_stream2.rewind()?;
output_stream2.read_to_end(&mut bytes)?;
assert_eq!(SAMPLE1, bytes);
Ok(())
}
#[test]
fn test_write_insert_two_blocks() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let mut output_stream1 = Cursor::new(Vec::with_capacity(SAMPLE1.len()));
let gif_io = GifIO {};
let test_block = Block::ApplicationExtension(ApplicationExtension {
identifier: *b"12345678",
authentication_code: [0, 0, 0],
data_sub_blocks: DataSubBlocks::empty(),
});
gif_io.insert_block(&mut stream, &mut output_stream1, &test_block)?;
let mut output_stream2 = Cursor::new(Vec::with_capacity(SAMPLE1.len()));
gif_io.insert_block(&mut output_stream1, &mut output_stream2, &test_block)?;
let blocks: Vec<_> = Blocks::new(&mut output_stream2)?.collect::<Result<_>>()?;
assert_eq!(
blocks.get(3),
Some(&BlockMarker {
start: 781,
len: 15,
block: test_block.clone()
})
);
assert_eq!(
blocks.get(4),
Some(&BlockMarker {
start: 796,
len: 15,
block: test_block
})
);
Ok(())
}
#[test]
fn test_write_bytes() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let gif_io = GifIO {};
assert!(matches!(
gif_io.read_c2pa(&mut stream),
Err(Error::JumbfNotFound)
));
let mut output_stream = Cursor::new(Vec::with_capacity(SAMPLE1.len() + 15 + 7));
let random_bytes = [1, 2, 3, 4, 3, 2, 1];
gif_io.write_c2pa(&mut stream, &mut output_stream, &random_bytes)?;
let data_written = gif_io.read_c2pa(&mut output_stream)?;
assert_eq!(data_written, random_bytes);
Ok(())
}
#[test]
fn test_write_bytes_replace() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let gif_io = GifIO {};
assert!(matches!(
gif_io.read_c2pa(&mut stream),
Err(Error::JumbfNotFound)
));
let mut output_stream1 = Cursor::new(Vec::with_capacity(SAMPLE1.len() + 15 + 7));
let random_bytes = [1, 2, 3, 4, 3, 2, 1];
gif_io.write_c2pa(&mut stream, &mut output_stream1, &random_bytes)?;
let data_written = gif_io.read_c2pa(&mut output_stream1)?;
assert_eq!(data_written, random_bytes);
let mut output_stream2 = Cursor::new(Vec::with_capacity(SAMPLE1.len() + 15 + 5));
let random_bytes = [3, 2, 1, 2, 3];
gif_io.write_c2pa(&mut output_stream1, &mut output_stream2, &random_bytes)?;
let data_written = gif_io.read_c2pa(&mut output_stream2)?;
assert_eq!(data_written, random_bytes);
let mut bytes = Vec::new();
stream.rewind()?;
stream.read_to_end(&mut bytes)?;
assert_eq!(SAMPLE1, bytes);
Ok(())
}
#[test]
fn test_data_hash_locations() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let gif_io = GifIO {};
let obj_locations = gif_io.get_object_locations(&mut stream)?;
assert_eq!(
obj_locations.first(),
Some(&ObjectLocations {
offset: 0,
length: 780,
htype: ObjectType::Other,
})
);
assert_eq!(
obj_locations.get(1),
Some(&ObjectLocations {
offset: 781,
length: 1,
htype: ObjectType::C2pa,
})
);
assert_eq!(
obj_locations.get(2),
Some(&ObjectLocations {
offset: 782,
length: (SAMPLE1.len() - 781) as u64,
htype: ObjectType::Other,
})
);
assert_eq!(obj_locations.len(), 3);
let mut output_stream1 = Cursor::new(Vec::with_capacity(SAMPLE1.len() + 15 + 4));
gif_io.write_c2pa(&mut stream, &mut output_stream1, &[1, 2, 3, 4])?;
let mut obj_locations = gif_io.get_object_locations(&mut output_stream1)?;
obj_locations.sort_by_key(|pos| pos.offset);
assert_eq!(
obj_locations.first(),
Some(&ObjectLocations {
offset: 0,
length: 780,
htype: ObjectType::Other,
})
);
assert_eq!(
obj_locations.get(1),
Some(&ObjectLocations {
offset: 781,
length: 20,
htype: ObjectType::C2pa,
})
);
assert_eq!(
obj_locations.get(2),
Some(&ObjectLocations {
offset: 801,
length: (SAMPLE1.len() - 781) as u64,
htype: ObjectType::Other,
})
);
assert_eq!(obj_locations.len(), 3);
Ok(())
}
#[test]
fn test_box_hash_locations() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let gif_io = GifIO {};
let box_map = gif_io.get_box_map(&mut stream)?;
assert_eq!(
box_map.first(),
Some(&BoxMap::new(vec!["GIF89a".to_owned()], 0, 6))
);
assert_eq!(
box_map.get(box_map.len() / 2),
Some(&BoxMap::new(vec!["2C".to_owned()], 368494, 778))
);
assert_eq!(
box_map.last(),
Some(&BoxMap::new(
vec!["3B".to_owned()],
SAMPLE1.len() as u64 - 1,
1
))
);
assert_eq!(box_map.len(), 276);
Ok(())
}
#[test]
fn test_composed_manifest() -> Result<()> {
let gif_io = GifIO {};
let block = gif_io.compose_manifest(&[1, 2, 3], "")?;
assert_eq!(
block,
vec![33, 255, 11, 67, 50, 80, 65, 95, 71, 73, 70, 1, 0, 0, 3, 1, 2, 3, 0]
);
Ok(())
}
#[test]
fn test_xmp_short_trailer_does_not_panic() {
let gif_io = GifIO {};
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(b"GIF89a");
data.extend_from_slice(&[0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
data.push(0x21); data.push(0xff); data.push(0x0b); data.extend_from_slice(b"XMP Data"); data.extend_from_slice(&[0x58, 0x4d, 0x50]); data.push(0x0a); data.extend_from_slice(&[0u8; 10]); data.push(0x00); data.push(0x3b);
let mut stream = Cursor::new(data);
assert_eq!(gif_io.read_xmp(&mut stream), None);
}
#[test]
fn test_remote_ref() -> Result<()> {
let mut stream = Cursor::new(SAMPLE1);
let gif_io = GifIO {};
assert_eq!(gif_io.read_xmp(&mut stream), None);
let mut output_stream1 = Cursor::new(Vec::with_capacity(SAMPLE1.len()));
gif_io.write_remote_manifest_url(&mut stream, &mut output_stream1, "Test")?;
let xmp = gif_io.read_xmp(&mut output_stream1).unwrap();
let p = extract_provenance(&xmp).unwrap();
assert_eq!(&p, "Test");
Ok(())
}
}