Skip to content

Commit 4c651af

Browse files
committed
add baseline v2 api support
1 parent 2304265 commit 4c651af

File tree

15 files changed

+1358
-48
lines changed

15 files changed

+1358
-48
lines changed

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rust-analyzer.linkedProjects": [
3+
"./Cargo.toml"
4+
],
5+
"rust-analyzer.showUnlinkedFileNotification": false
6+
}

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ name = "c2pa"
1010
crate-type = ["cdylib"]
1111

1212
[dependencies]
13-
c2pa-c = { git = "https://github.com/contentauth/c2pa-c.git", branch = "main" }
14-
serde = { version = "1.0", features = ["derive"] }
13+
c2pa = {path="../c2pa-rs/sdk", features = ["file_io", "openssl_sign"]}
14+
pem = "3.0.3"
15+
serde = { version = "1.0.197", features = ["derive"] }
1516
serde_derive = "1.0"
1617
serde_json = "1.0"
1718
thiserror = "1.0.49"

src/c2pa.udl

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,64 @@ namespace c2pa {
1111
};
1212

1313
[Error]
14-
enum Error {
15-
"Assertion",
16-
"AssertionNotFound",
17-
"Decoding",
18-
"Encoding",
19-
"FileNotFound",
20-
"Io",
21-
"Json",
22-
"Manifest",
23-
"ManifestNotFound",
24-
"NotSupported",
25-
"Other",
26-
"NullParameter",
27-
"RemoteManifest",
28-
"ResourceNotFound",
29-
"Signature",
30-
"Verify"
14+
interface Error {
15+
Assertion(string reason);
16+
AssertionNotFound(string reason);
17+
Decoding(string reason);
18+
Encoding(string reason);
19+
FileNotFound(string reason);
20+
Io(string reason);
21+
Json(string reason);
22+
Manifest(string reason);
23+
ManifestNotFound(string reason);
24+
NotSupported(string reason);
25+
FileNotFound(string reason);
26+
Other(string reason);
27+
RemoteManifest(string reason);
28+
ResourceNotFound(string reason);
29+
RwLock();
30+
Signature(string reason);
31+
Verify(string reason);
32+
};
33+
34+
enum SigningAlg {
35+
"Es256",
36+
"Es384",
37+
"Es512",
38+
"Ps256",
39+
"Ps384",
40+
"Ps512",
41+
"Ed25519"
42+
};
43+
44+
enum SeekMode {
45+
"Start",
46+
"End",
47+
"Current"
48+
};
49+
50+
callback interface Stream {
51+
[Throws=Error]
52+
bytes read_stream(u64 length);
53+
54+
[Throws=Error]
55+
u64 seek_stream(i64 pos, SeekMode mode);
56+
57+
[Throws=Error]
58+
u64 write_stream(bytes data);
59+
};
60+
61+
interface Reader {
62+
constructor();
63+
64+
[Throws=Error]
65+
string read([ByRef] string format, [ByRef] Stream reader);
66+
67+
[Throws=Error]
68+
string json();
69+
70+
[Throws=Error]
71+
u64 resource([ByRef] string uri, [ByRef] Stream stream);
3172
};
3273

3374
dictionary SignerInfo {
@@ -37,3 +78,31 @@ dictionary SignerInfo {
3778
string? ta_url;
3879
};
3980

81+
dictionary SignerConfig {
82+
SigningAlg alg;
83+
bytes certs;
84+
string? time_authority_url = null;
85+
boolean use_ocsp = false;
86+
};
87+
88+
callback interface SignerCallback {
89+
[Throws=Error]
90+
bytes sign(bytes data);
91+
};
92+
93+
interface CallbackSigner {
94+
constructor(SignerCallback callback, SignerConfig config);
95+
};
96+
97+
interface Builder {
98+
constructor();
99+
100+
[Throws=Error]
101+
void with_json([ByRef] string json);
102+
103+
[Throws=Error]
104+
void add_resource([ByRef] string uri, [ByRef] Stream stream );
105+
106+
[Throws=Error]
107+
bytes sign([ByRef] string format, [ByRef] Stream input, [ByRef] Stream output ,[ByRef] CallbackSigner signer);
108+
};

src/error.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use thiserror::Error;
2+
pub type Result<T> = std::result::Result<T, Error>;
3+
4+
#[derive(Error, Debug)]
5+
/// Defines all possible errors that can occur in this library
6+
pub enum Error {
7+
#[error("Assertion {reason}")]
8+
Assertion { reason: String },
9+
#[error("AssertionNotFound {reason}")]
10+
AssertionNotFound { reason: String },
11+
#[error("Decoding {reason}")]
12+
Decoding{ reason: String },
13+
#[error("Encoding {reason}")]
14+
Encoding { reason: String },
15+
#[error("FileNotFound{reason}")]
16+
FileNotFound { reason: String },
17+
#[error("Io {reason}")]
18+
Io { reason: String },
19+
#[error("Json {reason}")]
20+
Json { reason: String },
21+
#[error("Manifest {reason}")]
22+
Manifest { reason: String },
23+
#[error("ManifestNotFound {reason}")]
24+
ManifestNotFound { reason: String },
25+
#[error("NotSupported {reason}")]
26+
NotSupported { reason: String },
27+
#[error("Other {reason}")]
28+
Other { reason: String },
29+
#[error("Remote {reason}")]
30+
RemoteManifest { reason: String },
31+
#[error("ResourceNotFound {reason}")]
32+
ResourceNotFound { reason: String },
33+
#[error("RwLock")]
34+
RwLock,
35+
#[error("Signature {reason}")]
36+
Signature { reason: String },
37+
#[error("Verify{reason}")]
38+
Verify { reason: String },
39+
}
40+
41+
impl Error {
42+
// Convert c2pa errors to published API errors
43+
#[allow(unused_variables)]
44+
pub(crate) fn from_c2pa_error(err: c2pa::Error) -> Self {
45+
use c2pa::Error::*;
46+
let err_str = err.to_string();
47+
match err {
48+
c2pa::Error::AssertionMissing { url } => Self::AssertionNotFound{ reason: "".to_string()},
49+
AssertionInvalidRedaction
50+
| AssertionRedactionNotFound
51+
| AssertionUnsupportedVersion => Self::Assertion{ reason: err_str},
52+
ClaimAlreadySigned
53+
| ClaimUnsigned
54+
| ClaimMissingSignatureBox
55+
| ClaimMissingIdentity
56+
| ClaimVersion
57+
| ClaimInvalidContent
58+
| ClaimMissingHardBinding
59+
| ClaimSelfRedact
60+
| ClaimDisallowedRedaction
61+
| UpdateManifestInvalid
62+
| TooManyManifestStores => Self::Manifest{ reason: err_str},
63+
ClaimMissing { label } => Self::ManifestNotFound{ reason: err_str},
64+
AssertionDecoding(_) | ClaimDecoding => Self::Decoding{ reason: err_str},
65+
AssertionEncoding | XmlWriteError | ClaimEncoding => Self::Encoding{ reason: err_str},
66+
InvalidCoseSignature { coset_error } => Self::Signature{ reason: err_str},
67+
CoseSignatureAlgorithmNotSupported
68+
| CoseMissingKey
69+
| CoseX5ChainMissing
70+
| CoseInvalidCert
71+
| CoseSignature
72+
| CoseVerifier
73+
| CoseCertExpiration
74+
| CoseCertRevoked
75+
| CoseInvalidTimeStamp
76+
| CoseTimeStampValidity
77+
| CoseTimeStampMismatch
78+
| CoseTimeStampGeneration
79+
| CoseTimeStampAuthority
80+
| CoseSigboxTooSmall
81+
| InvalidEcdsaSignature => Self::Signature{ reason: err_str},
82+
RemoteManifestFetch(_) | RemoteManifestUrl(_) => Self::RemoteManifest{ reason: err_str},
83+
JumbfNotFound => Self::ManifestNotFound{ reason: err_str},
84+
BadParam(_) | MissingFeature(_) => Self::Other{ reason: err_str},
85+
IoError(_) => Self::Io{ reason: err_str},
86+
JsonError(e) => Self::Json{ reason: err_str},
87+
NotFound | ResourceNotFound(_) | MissingDataBox => Self::ResourceNotFound{ reason: err_str},
88+
FileNotFound(_) => Self::FileNotFound{ reason: err_str},
89+
UnsupportedType => Self::NotSupported{ reason: err_str},
90+
ClaimVerification(_) | InvalidClaim(_) | JumbfParseError(_) => Self::Verify{ reason: err_str},
91+
#[cfg(feature = "add_thumbnails")]
92+
ImageError => Self::ImageError(err_str),
93+
_ => Self::Other{ reason: err_str},
94+
}
95+
}
96+
}
97+
98+
impl From<uniffi::UnexpectedUniFFICallbackError> for Error {
99+
fn from(err: uniffi::UnexpectedUniFFICallbackError) -> Self {
100+
Self::Other {
101+
reason: err.reason.clone(),
102+
}
103+
}
104+
}
105+
106+
impl From<c2pa::Error> for Error {
107+
fn from(err: c2pa::Error) -> Self {
108+
Self::from_c2pa_error(err)
109+
}
110+
}
111+
112+
impl From<std::io::Error> for Error {
113+
fn from(err: std::io::Error) -> Self {
114+
Self::Io { reason: err.to_string()}
115+
}
116+
}
117+

src/json_api.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
// Unless required by applicable law or agreed to in writing,
7+
// this software is distributed on an "AS IS" BASIS, WITHOUT
8+
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
9+
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
10+
// specific language governing permissions and limitations under
11+
// each license.
12+
13+
use c2pa::{Ingredient, Manifest, ManifestStore};
14+
15+
use crate::{Error, Result, SignerInfo};
16+
17+
/// Returns the version of the C2PA library
18+
pub fn sdk_version() -> String {
19+
format!(
20+
"{}/{} {}/{}",
21+
env!("CARGO_PKG_NAME"),
22+
env!("CARGO_PKG_VERSION"),
23+
c2pa::NAME,
24+
c2pa::VERSION
25+
)
26+
}
27+
28+
/// Returns ManifestStore JSON string from a file path.
29+
///
30+
/// If data_dir is provided, any thumbnail or c2pa data will be written to that folder.
31+
/// Any Validation errors will be reported in the validation_status field.
32+
///
33+
pub fn read_file(path: &str, data_dir: Option<String>) -> Result<String> {
34+
Ok(match data_dir {
35+
Some(dir) => ManifestStore::from_file_with_resources(path, &dir),
36+
None => ManifestStore::from_file(path),
37+
}
38+
.map_err(Error::from_c2pa_error)?
39+
.to_string())
40+
}
41+
42+
/// Returns an Ingredient JSON string from a file path.
43+
///
44+
/// Any thumbnail or c2pa data will be written to data_dir if provided
45+
pub fn read_ingredient_file(path: &str, data_dir: &str) -> Result<String> {
46+
Ok(Ingredient::from_file_with_folder(path, data_dir)
47+
.map_err(Error::from_c2pa_error)?
48+
.to_string())
49+
}
50+
51+
/// Adds a manifest to the source file and writes the result to the destination file.
52+
/// Also returns the binary manifest data for optional cloud storage
53+
/// A manifest definition must be supplied
54+
/// Signer information must also be supplied
55+
///
56+
/// Any file paths in the manifest will be read relative to the source file
57+
pub fn sign_file(
58+
source: &str,
59+
dest: &str,
60+
manifest_json: &str,
61+
signer_info: &SignerInfo,
62+
data_dir: Option<String>,
63+
) -> Result<Vec<u8>> {
64+
let mut manifest = Manifest::from_json(manifest_json).map_err(Error::from_c2pa_error)?;
65+
66+
// if data_dir is provided, set the base path for the manifest
67+
if let Some(path) = data_dir {
68+
manifest
69+
.with_base_path(path)
70+
.map_err(Error::from_c2pa_error)?;
71+
}
72+
73+
// If the source file has a manifest store, and no parent is specified, treat the source's manifest store as the parent.
74+
if manifest.parent().is_none() {
75+
let source_ingredient = Ingredient::from_file(source).map_err(Error::from_c2pa_error)?;
76+
if source_ingredient.manifest_data().is_some() {
77+
manifest
78+
.set_parent(source_ingredient)
79+
.map_err(Error::from_c2pa_error)?;
80+
}
81+
}
82+
83+
let signer = signer_info.signer()?;
84+
manifest
85+
.embed(&source, &dest, &*signer)
86+
.map_err(Error::from_c2pa_error)
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::*;
92+
use std::{fs::remove_dir_all, path::PathBuf};
93+
94+
/// returns a path to a file in the fixtures folder
95+
pub fn test_path(path: &str) -> String {
96+
let base = env!("CARGO_MANIFEST_DIR");
97+
format!("{}/{}", base, path)
98+
}
99+
100+
#[test]
101+
fn test_verify_from_file_no_base() {
102+
let path = test_path("tests/fixtures/C.jpg");
103+
let result = read_file(&path, None);
104+
assert!(result.is_ok());
105+
let json_report = result.unwrap();
106+
println!("{}", json_report);
107+
assert!(json_report.contains("C.jpg"));
108+
assert!(!json_report.contains("validation_status"));
109+
}
110+
111+
#[test]
112+
fn test_read_from_file_with_base() {
113+
let path = test_path("tests/fixtures/C.jpg");
114+
let data_dir = "target/data_dir";
115+
if PathBuf::from(data_dir).exists() {
116+
remove_dir_all(data_dir).unwrap();
117+
}
118+
let result = read_file(&path, Some(data_dir.to_owned()));
119+
assert!(result.is_ok());
120+
let json_report = result.unwrap();
121+
println!("{}", json_report);
122+
assert!(json_report.contains("C.jpg"));
123+
assert!(PathBuf::from(data_dir).exists());
124+
assert!(json_report.contains("thumbnail"));
125+
}
126+
}

0 commit comments

Comments
 (0)