mp3_io.rs (14698B)
1 // Copyright 2023 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 use std::{ 15 fs::{File, OpenOptions}, 16 io::{Cursor, Seek, SeekFrom, Write}, 17 path::Path, 18 }; 19 20 use byteorder::{BigEndian, ReadBytesExt}; 21 use conv::ValueFrom; 22 use id3::{frame::EncapsulatedObject, *}; 23 use memchr::memmem; 24 use tempfile::Builder; 25 26 use crate::{ 27 asset_io::{ 28 rename_or_copy, AssetIO, AssetPatch, CAIRead, CAIReadWrapper, CAIReadWrite, 29 CAIReadWriteWrapper, CAIReader, CAIWriter, HashBlockObjectType, HashObjectPositions, 30 RemoteRefEmbed, 31 }, 32 error::{Error, Result}, 33 }; 34 35 static SUPPORTED_TYPES: [&str; 2] = ["mp3", "audio/mpeg"]; 36 37 const GEOB_FRAME_MIME_TYPE: &str = "application/x-c2pa-manifest-store"; 38 const GEOB_FRAME_FILE_NAME: &str = "c2pa"; 39 const GEOB_FRAME_DESCRIPTION: &str = "c2pa manifest store"; 40 41 struct ID3V2Header { 42 _version_major: u8, 43 _version_minor: u8, 44 _flags: u8, 45 tag_size: u32, 46 } 47 48 impl ID3V2Header { 49 pub fn read_header(reader: &mut dyn CAIRead) -> Result<ID3V2Header> { 50 let mut header = [0; 10]; 51 reader.read_exact(&mut header).map_err(Error::IoError)?; 52 53 if &header[0..3] != b"ID3" { 54 return Err(Error::UnsupportedType); 55 } 56 57 let (version_major, version_minor) = (header[3], header[4]); 58 if !(2..=4).contains(&version_major) { 59 return Err(Error::UnsupportedType); 60 } 61 62 let flags = header[5]; 63 64 let mut size_reader = Cursor::new(&header[6..10]); 65 let encoded_tag_size = size_reader 66 .read_u32::<BigEndian>() 67 .map_err(|_err| Error::InvalidAsset("could not read mp3 tag size".to_string()))?; 68 let tag_size = ID3V2Header::decode_tag_size(encoded_tag_size); 69 70 Ok(ID3V2Header { 71 _version_major: version_major, 72 _version_minor: version_minor, 73 _flags: flags, 74 tag_size, 75 }) 76 } 77 78 pub const fn get_size(&self) -> u32 { 79 self.tag_size + 10 80 } 81 82 const fn decode_tag_size(n: u32) -> u32 { 83 n & 0xff | (n & 0xff00) >> 1 | (n & 0xff0000) >> 2 | (n & 0xff000000) >> 3 84 } 85 } 86 87 fn get_manifest_pos(input_stream: &mut dyn CAIRead) -> Option<(u64, u32)> { 88 input_stream.rewind().ok()?; 89 let header = ID3V2Header::read_header(input_stream).ok()?; 90 input_stream.rewind().ok()?; 91 92 let reader = CAIReadWrapper { 93 reader: input_stream, 94 }; 95 96 if let Ok(tag) = Tag::read_from(reader) { 97 let mut manifests = Vec::new(); 98 99 for eo in tag.encapsulated_objects() { 100 if eo.mime_type == GEOB_FRAME_MIME_TYPE { 101 manifests.push(eo.data.clone()); 102 } 103 } 104 105 if manifests.len() == 1 { 106 input_stream.rewind().ok()?; 107 108 let mut tag_bytes = vec![0u8; header.get_size() as usize]; 109 input_stream.read_exact(tag_bytes.as_mut_slice()).ok()?; 110 111 let pos = memmem::find(&tag_bytes, &manifests[0])?; 112 113 return Some((pos as u64, manifests[0].len() as u32)); 114 } 115 } 116 None 117 } 118 119 pub struct Mp3IO { 120 _mp3_format: String, 121 } 122 123 impl CAIReader for Mp3IO { 124 fn read_cai(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<u8>> { 125 let mut manifest: Option<Vec<u8>> = None; 126 127 if let Ok(tag) = Tag::read_from(input_stream) { 128 for eo in tag.encapsulated_objects() { 129 if eo.mime_type == GEOB_FRAME_MIME_TYPE { 130 match manifest { 131 Some(_) => { 132 return Err(Error::TooManyManifestStores); 133 } 134 None => manifest = Some(eo.data.clone()), 135 } 136 } 137 } 138 } 139 140 manifest.ok_or(Error::JumbfNotFound) 141 } 142 143 // Get XMP block 144 fn read_xmp(&self, _input_stream: &mut dyn CAIRead) -> Option<String> { 145 None 146 } 147 } 148 149 fn add_required_frame( 150 asset_type: &str, 151 input_stream: &mut dyn CAIRead, 152 output_stream: &mut dyn CAIReadWrite, 153 ) -> Result<()> { 154 let mp3io = Mp3IO::new(asset_type); 155 156 input_stream.rewind()?; 157 158 match mp3io.read_cai(input_stream) { 159 Ok(_) => { 160 // just clone 161 input_stream.rewind()?; 162 output_stream.rewind()?; 163 std::io::copy(input_stream, output_stream)?; 164 Ok(()) 165 } 166 Err(_) => { 167 input_stream.rewind()?; 168 mp3io.write_cai(input_stream, output_stream, &[1, 2, 3, 4]) // save arbitrary data 169 } 170 } 171 } 172 173 impl AssetIO for Mp3IO { 174 fn new(mp3_format: &str) -> Self { 175 Mp3IO { 176 _mp3_format: mp3_format.to_string(), 177 } 178 } 179 180 fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { 181 Box::new(Mp3IO::new(asset_type)) 182 } 183 184 fn get_reader(&self) -> &dyn CAIReader { 185 self 186 } 187 188 fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> { 189 Some(Box::new(Mp3IO::new(asset_type))) 190 } 191 192 fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> { 193 Some(self) 194 } 195 196 fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> { 197 let mut f = File::open(asset_path)?; 198 self.read_cai(&mut f) 199 } 200 201 fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> { 202 let mut input_stream = std::fs::OpenOptions::new() 203 .read(true) 204 .write(true) 205 .open(asset_path) 206 .map_err(Error::IoError)?; 207 208 let mut temp_file = Builder::new() 209 .prefix("c2pa_temp") 210 .rand_bytes(5) 211 .tempfile()?; 212 213 self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?; 214 215 // copy temp file to asset 216 rename_or_copy(temp_file, asset_path) 217 } 218 219 fn get_object_locations( 220 &self, 221 asset_path: &std::path::Path, 222 ) -> Result<Vec<HashObjectPositions>> { 223 let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?; 224 225 self.get_object_locations_from_stream(&mut f) 226 } 227 228 fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { 229 self.save_cai_store(asset_path, &[]) 230 } 231 232 fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { 233 None 234 } 235 236 fn supported_types(&self) -> &[&str] { 237 &SUPPORTED_TYPES 238 } 239 } 240 241 impl CAIWriter for Mp3IO { 242 fn write_cai( 243 &self, 244 input_stream: &mut dyn CAIRead, 245 output_stream: &mut dyn CAIReadWrite, 246 store_bytes: &[u8], 247 ) -> Result<()> { 248 let header = ID3V2Header::read_header(input_stream)?; 249 input_stream.rewind()?; 250 251 let mut out_tag = Tag::new(); 252 253 // wrapper to protect input stream from being gobbled 254 let reader = CAIReadWrapper { 255 reader: input_stream, 256 }; 257 258 if let Ok(tag) = Tag::read_from(reader) { 259 for f in tag.frames() { 260 match f.content() { 261 // remove existing manifest keeping existing frames 262 Content::EncapsulatedObject(eo) => { 263 if eo.mime_type != "application/x-c2pa-manifest-store" { 264 out_tag.add_frame(f.clone()); 265 } 266 } 267 _ => { 268 out_tag.add_frame(f.clone()); 269 } 270 } 271 } 272 } 273 274 // only add new tags 275 if !store_bytes.is_empty() { 276 // Add new manifest store 277 let frame = Frame::with_content( 278 "GEOB", 279 Content::EncapsulatedObject(EncapsulatedObject { 280 mime_type: GEOB_FRAME_MIME_TYPE.to_string(), 281 filename: GEOB_FRAME_FILE_NAME.to_string(), 282 description: GEOB_FRAME_DESCRIPTION.to_string(), 283 data: store_bytes.to_vec(), 284 }), 285 ); 286 287 out_tag.add_frame(frame); 288 } 289 290 // wrapper to protect output stream from being gobbled 291 let writer = CAIReadWriteWrapper { 292 reader_writer: output_stream, 293 }; 294 295 // write new tag to output stream 296 out_tag 297 .write_to(writer, Version::Id3v24) 298 .map_err(|_e| Error::EmbeddingError)?; 299 300 // skip past old ID3V2 301 input_stream.seek(SeekFrom::Start(header.get_size() as u64))?; 302 303 // copy source data to output 304 std::io::copy(input_stream, output_stream)?; 305 306 Ok(()) 307 } 308 309 fn get_object_locations_from_stream( 310 &self, 311 input_stream: &mut dyn CAIRead, 312 ) -> Result<Vec<HashObjectPositions>> { 313 let output_buf: Vec<u8> = Vec::new(); 314 let mut output_stream = Cursor::new(output_buf); 315 316 add_required_frame(&self._mp3_format, input_stream, &mut output_stream)?; 317 318 let mut positions: Vec<HashObjectPositions> = Vec::new(); 319 320 let (manifest_pos, manifest_len) = 321 get_manifest_pos(&mut output_stream).ok_or(Error::EmbeddingError)?; 322 323 positions.push(HashObjectPositions { 324 offset: usize::value_from(manifest_pos) 325 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, 326 length: usize::value_from(manifest_len) 327 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, 328 htype: HashBlockObjectType::Cai, 329 }); 330 331 // add hash of chunks before cai 332 positions.push(HashObjectPositions { 333 offset: 0, 334 length: usize::value_from(manifest_pos) 335 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, 336 htype: HashBlockObjectType::Other, 337 }); 338 339 // add position from cai to end 340 let end = u64::value_from(manifest_pos) 341 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))? 342 + u64::value_from(manifest_len) 343 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; 344 let file_end = output_stream.seek(SeekFrom::End(0))?; 345 positions.push(HashObjectPositions { 346 offset: usize::value_from(end) 347 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, // len of cai 348 length: usize::value_from(file_end - end) 349 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, 350 htype: HashBlockObjectType::Other, 351 }); 352 353 Ok(positions) 354 } 355 356 fn remove_cai_store_from_stream( 357 &self, 358 input_stream: &mut dyn CAIRead, 359 output_stream: &mut dyn CAIReadWrite, 360 ) -> Result<()> { 361 self.write_cai(input_stream, output_stream, &[]) 362 } 363 } 364 365 impl AssetPatch for Mp3IO { 366 fn patch_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> { 367 let mut asset = OpenOptions::new() 368 .write(true) 369 .read(true) 370 .create(false) 371 .open(asset_path)?; 372 373 let (manifest_pos, manifest_len) = 374 get_manifest_pos(&mut asset).ok_or(Error::EmbeddingError)?; 375 376 if store_bytes.len() == manifest_len as usize { 377 asset.seek(SeekFrom::Start(manifest_pos))?; 378 asset.write_all(store_bytes)?; 379 Ok(()) 380 } else { 381 Err(Error::InvalidAsset( 382 "patch_cai_store store size mismatch.".to_string(), 383 )) 384 } 385 } 386 } 387 388 #[cfg(test)] 389 pub mod tests { 390 #![allow(clippy::expect_used)] 391 #![allow(clippy::panic)] 392 #![allow(clippy::unwrap_used)] 393 394 use tempfile::tempdir; 395 396 use super::*; 397 use crate::utils::{ 398 hash_utils::vec_compare, 399 test::{fixture_path, temp_dir_path}, 400 }; 401 402 #[test] 403 fn test_write_mp3() { 404 let more_data = "some more test data".as_bytes(); 405 let source = fixture_path("sample1.mp3"); 406 407 let mut success = false; 408 if let Ok(temp_dir) = tempdir() { 409 let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3"); 410 411 if let Ok(_size) = std::fs::copy(source, &output) { 412 let mp3_io = Mp3IO::new("mp3"); 413 414 if let Ok(()) = mp3_io.save_cai_store(&output, more_data) { 415 if let Ok(read_test_data) = mp3_io.read_cai_store(&output) { 416 assert!(vec_compare(more_data, &read_test_data)); 417 success = true; 418 } 419 } 420 } 421 } 422 assert!(success) 423 } 424 425 #[test] 426 fn test_patch_write_mp3() { 427 let test_data = "some test data".as_bytes(); 428 let source = fixture_path("sample1.mp3"); 429 430 let mut success = false; 431 if let Ok(temp_dir) = tempdir() { 432 let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3"); 433 434 if let Ok(_size) = std::fs::copy(source, &output) { 435 let mp3_io = Mp3IO::new("mp3"); 436 437 if let Ok(()) = mp3_io.save_cai_store(&output, test_data) { 438 if let Ok(source_data) = mp3_io.read_cai_store(&output) { 439 // create replacement data of same size 440 let mut new_data = vec![0u8; source_data.len()]; 441 new_data[..test_data.len()].copy_from_slice(test_data); 442 mp3_io.patch_cai_store(&output, &new_data).unwrap(); 443 444 let replaced = mp3_io.read_cai_store(&output).unwrap(); 445 446 assert_eq!(new_data, replaced); 447 448 success = true; 449 } 450 } 451 } 452 } 453 assert!(success) 454 } 455 456 #[test] 457 fn test_remove_c2pa() { 458 let source = fixture_path("sample1.mp3"); 459 460 let temp_dir = tempdir().unwrap(); 461 let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3"); 462 463 std::fs::copy(source, &output).unwrap(); 464 let mp3_io = Mp3IO::new("wav"); 465 466 mp3_io.remove_cai_store(&output).unwrap(); 467 468 // read back in asset, JumbfNotFound is expected since it was removed 469 match mp3_io.read_cai_store(&output) { 470 Err(Error::JumbfNotFound) => (), 471 _ => unreachable!(), 472 } 473 } 474 }