ruby-c2pa 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ crate-type = ["cdylib"]
9
9
 
10
10
  [dependencies]
11
11
  magnus = { version = "0.8", features = [] }
12
- c2pa = { version = "0.78", features = ["file_io"] }
12
+ c2pa = { version = "0.90", features = ["file_io"] }
13
13
  serde_json = "1"
14
14
 
15
15
  [profile.release]
@@ -1,5 +1,5 @@
1
1
  use std::path::Path;
2
- use c2pa::{create_signer, Builder, Reader, SigningAlg};
2
+ use c2pa::{create_signer, Builder, BuilderIntent, Reader, SigningAlg};
3
3
  use magnus::{function, prelude::*, Error, Ruby};
4
4
 
5
5
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -22,6 +22,22 @@ fn alg_from_str(alg: &str) -> Result<SigningAlg, String> {
22
22
 
23
23
  // ─── Core logic ──────────────────────────────────────────────────────────────
24
24
 
25
+ // A c2pa.opened action has to reference its parent ingredient by hashed URI,
26
+ // and that hash is computed over the ingredient assertion as c2pa-rs
27
+ // serialises it. Ruby cannot construct one. Declaring the intent instead lets
28
+ // the SDK derive the parent ingredient from the source and wire the action to
29
+ // it, which is the only way the edit workflow can be expressed.
30
+ fn intent_from_str(intent: &str) -> Result<BuilderIntent, String> {
31
+ match intent.to_lowercase().as_str() {
32
+ "edit" => Ok(BuilderIntent::Edit),
33
+ "update" => Ok(BuilderIntent::Update),
34
+ _ => Err(format!(
35
+ "Unknown intent: '{}'. Valid options: edit, update",
36
+ intent
37
+ )),
38
+ }
39
+ }
40
+
25
41
  fn do_sign_file(
26
42
  source_path: &str,
27
43
  dest_path: &str,
@@ -29,6 +45,7 @@ fn do_sign_file(
29
45
  key_path: &str,
30
46
  alg_str: &str,
31
47
  manifest_json: Option<&str>,
48
+ intent_str: Option<&str>,
32
49
  ) -> Result<(), Box<dyn std::error::Error>> {
33
50
  let cert = std::fs::read(cert_path)
34
51
  .map_err(|e| format!("Cannot read certificate '{}': {}", cert_path, e))?;
@@ -50,6 +67,10 @@ fn do_sign_file(
50
67
  let mut builder = Builder::from_json(json)
51
68
  .map_err(|e| format!("Invalid manifest JSON: {}", e))?;
52
69
 
70
+ if let Some(intent) = intent_str {
71
+ builder.set_intent(intent_from_str(intent)?);
72
+ }
73
+
53
74
  builder.sign_file(&*signer, source_path, dest_path)
54
75
  .map_err(|e| format!("Signing failed: {}", e))?;
55
76
 
@@ -71,10 +92,11 @@ fn sign_file(
71
92
  key: String,
72
93
  alg: Option<String>,
73
94
  manifest_json: Option<String>,
95
+ intent: Option<String>,
74
96
  ) -> Result<String, Error> {
75
97
  let alg_str = alg.as_deref().unwrap_or("es256");
76
98
 
77
- do_sign_file(&source, &dest, &cert, &key, alg_str, manifest_json.as_deref())
99
+ do_sign_file(&source, &dest, &cert, &key, alg_str, manifest_json.as_deref(), intent.as_deref())
78
100
  .map_err(|e| Error::new(Ruby::get().expect("called from Ruby thread").exception_runtime_error(), e.to_string()))?;
79
101
 
80
102
  Ok(dest)
@@ -96,7 +118,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
96
118
  let c2pa = ruby.define_module("C2PA")?;
97
119
  let native = c2pa.define_module("Native")?;
98
120
 
99
- native.define_singleton_method("sign_file", function!(sign_file, 6))?;
121
+ native.define_singleton_method("sign_file", function!(sign_file, 7))?;
100
122
  native.define_singleton_method("read_file", function!(read_file, 1))?;
101
123
  native.define_singleton_method("sdk_version", function!(sdk_version, 0))?;
102
124
 
@@ -0,0 +1,72 @@
1
+ module C2PA
2
+ # URIs from the IPTC Digital Source Type NewsCodes vocabulary.
3
+ #
4
+ # The C2PA specification requires every c2pa.created action to declare one of
5
+ # these, so consumers can tell how the asset came into being. Pick the one that
6
+ # honestly describes the asset — this is a provenance claim, and c2pa-rs will
7
+ # not second-guess the value you supply.
8
+ #
9
+ # @see https://cv.iptc.org/newscodes/digitalsourcetype/
10
+ module DigitalSourceTypes
11
+ BASE = "http://cv.iptc.org/newscodes/digitalsourcetype"
12
+
13
+ # Declining to claim a source type, rather than guessing at one.
14
+ #
15
+ # c2pa-rs requires c2pa.created to carry a digitalSourceType but accepts
16
+ # any string, so a wrong value validates silently. When the origin is not
17
+ # known — a generic signing service, say — this says so, instead of
18
+ # asserting something untrue. It is the value c2pa-rs uses throughout its
19
+ # own fixtures.
20
+ UNSPECIFIED = "http://c2pa.org/digitalsourcetype/empty"
21
+
22
+ # Captured from real life
23
+ DIGITAL_CAPTURE = "#{BASE}/digitalCapture"
24
+ COMPUTATIONAL_CAPTURE = "#{BASE}/computationalCapture"
25
+ SCREEN_CAPTURE = "#{BASE}/screenCapture"
26
+ VIRTUAL_RECORDING = "#{BASE}/virtualRecording"
27
+
28
+ # Digitised from a physical medium
29
+ NEGATIVE_FILM = "#{BASE}/negativeFilm"
30
+ POSITIVE_FILM = "#{BASE}/positiveFilm"
31
+ PRINT = "#{BASE}/print"
32
+
33
+ # Human- or software-authored
34
+ HUMAN_EDITS = "#{BASE}/humanEdits"
35
+ DIGITAL_CREATION = "#{BASE}/digitalCreation"
36
+ ALGORITHMICALLY_ENHANCED = "#{BASE}/algorithmicallyEnhanced"
37
+ DATA_DRIVEN_MEDIA = "#{BASE}/dataDrivenMedia"
38
+ ALGORITHMIC_MEDIA = "#{BASE}/algorithmicMedia"
39
+
40
+ # Generative AI
41
+ TRAINED_ALGORITHMIC_MEDIA = "#{BASE}/trainedAlgorithmicMedia"
42
+ COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA = "#{BASE}/compositeWithTrainedAlgorithmicMedia"
43
+ COMPOSITE_SYNTHETIC = "#{BASE}/compositeSynthetic"
44
+
45
+ # Composites
46
+ COMPOSITE = "#{BASE}/composite"
47
+ COMPOSITE_CAPTURE = "#{BASE}/compositeCapture"
48
+
49
+ # Every type defined above. Supplying a URI outside this list is allowed —
50
+ # the vocabulary is extensible — but the values here cover the standard set.
51
+ ALL = [
52
+ UNSPECIFIED,
53
+ DIGITAL_CAPTURE,
54
+ COMPUTATIONAL_CAPTURE,
55
+ SCREEN_CAPTURE,
56
+ VIRTUAL_RECORDING,
57
+ NEGATIVE_FILM,
58
+ POSITIVE_FILM,
59
+ PRINT,
60
+ HUMAN_EDITS,
61
+ DIGITAL_CREATION,
62
+ ALGORITHMICALLY_ENHANCED,
63
+ DATA_DRIVEN_MEDIA,
64
+ ALGORITHMIC_MEDIA,
65
+ TRAINED_ALGORITHMIC_MEDIA,
66
+ COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA,
67
+ COMPOSITE_SYNTHETIC,
68
+ COMPOSITE,
69
+ COMPOSITE_CAPTURE
70
+ ].freeze
71
+ end
72
+ end
data/lib/c2pa/manifest.rb CHANGED
@@ -2,9 +2,44 @@ require "json"
2
2
 
3
3
  module C2PA
4
4
  class Manifest
5
- # @param title [String] human-readable title for this asset
6
- def initialize(title:)
5
+ # Intents this gem can express.
6
+ #
7
+ # :edit — this asset derives from a parent. c2pa-rs generates the parent
8
+ # ingredient from the source file and adds a c2pa.opened action
9
+ # wired to it by hashed URI.
10
+ #
11
+ # Omitting the intent produces a manifest for a newly created asset.
12
+ #
13
+ # c2pa-rs also has an :update intent, a restricted edit for non-editorial
14
+ # changes. It is not offered here because it requires an ingredient with
15
+ # real content, and add_ingredient records metadata only — signing with it
16
+ # fails with "ingredient file not found". Tracked separately.
17
+ INTENTS = %i[edit].freeze
18
+
19
+ # c2pa-rs records itself in a namespaced field alongside the generator
20
+ # name, so the gem does the same when an application supplies its own.
21
+ GEM_FIELD = "org.rubygems.ruby_c2pa".freeze
22
+
23
+ # @return [Symbol, nil] the builder intent, if any
24
+ attr_reader :intent
25
+
26
+ # @param title [String] human-readable title for this asset
27
+ # @param intent [Symbol, nil] :edit; omit for a new creation
28
+ # @param generator_name [String, nil] the application doing the signing.
29
+ # Defaults to this gem. Supplying it credits your application as the
30
+ # claim generator, with the gem recorded alongside.
31
+ # @param generator_version [String, nil] version of that application
32
+ # @raise [C2PA::InvalidManifestError] if the intent is not recognised
33
+ def initialize(title:, intent: nil, generator_name: nil, generator_version: nil)
34
+ unless intent.nil? || INTENTS.include?(intent)
35
+ raise InvalidManifestError,
36
+ "unknown intent #{intent.inspect}. Valid options: #{INTENTS.map(&:inspect).join(', ')}"
37
+ end
38
+
7
39
  @title = title
40
+ @intent = intent
41
+ @generator_name = generator_name
42
+ @generator_version = generator_version
8
43
  @actions = []
9
44
  @assertions = []
10
45
  @ingredients = []
@@ -26,6 +61,38 @@ module C2PA
26
61
  digital_source_type: nil,
27
62
  changed: nil,
28
63
  parameters: nil)
64
+ if action == Actions::OPENED
65
+ raise InvalidManifestError,
66
+ "#{Actions::OPENED} cannot be added directly. The specification requires it to " \
67
+ "reference a parentOf ingredient by hashed URI, and that hash is computed over " \
68
+ "the ingredient as c2pa-rs serialises it, so Ruby cannot construct one. Pass " \
69
+ "intent: :edit to C2PA::Manifest.new instead and the action will be added for you."
70
+ end
71
+
72
+ # Required as of c2pa-rs 0.90. Earlier versions accepted its absence, so
73
+ # manifests signed by releases before 0.3.0 are rejected by current
74
+ # verifiers. No default is supplied: c2pa-rs accepts any string here, so
75
+ # a guess would validate while asserting something untrue about where the
76
+ # asset came from. Use DigitalSourceTypes::UNSPECIFIED to decline.
77
+ if action == Actions::CREATED && to_s_or_nil(digital_source_type).nil?
78
+ raise InvalidManifestError,
79
+ "#{Actions::CREATED} requires a digital_source_type. Choose the value that " \
80
+ "describes how the asset was produced — for example " \
81
+ "C2PA::DigitalSourceTypes::DIGITAL_CAPTURE for a camera original, or " \
82
+ "TRAINED_ALGORITHMIC_MEDIA for generative AI. If the origin is genuinely " \
83
+ "unknown, use C2PA::DigitalSourceTypes::UNSPECIFIED rather than guessing."
84
+ end
85
+
86
+ # Also new in c2pa-rs 0.90.
87
+ if action == Actions::TRANSLATED
88
+ missing = %w[sourceLanguage targetLanguage].reject { |key| param_present?(parameters, key) }
89
+ unless missing.empty?
90
+ raise InvalidManifestError,
91
+ "#{Actions::TRANSLATED} requires #{missing.join(' and ')} in parameters, " \
92
+ "as RFC 5646 language codes"
93
+ end
94
+ end
95
+
29
96
  entry = { "action" => action }
30
97
  entry["when"] = when_time if when_time
31
98
  entry["softwareAgent"] = software_agent || "ruby-c2pa/#{VERSION}"
@@ -66,12 +133,14 @@ module C2PA
66
133
  # Serialize to the JSON structure expected by c2pa-rs.
67
134
  #
68
135
  # @return [String]
69
- # @raise [C2PA::InvalidManifestError] if no actions have been added
136
+ # @raise [C2PA::InvalidManifestError] if no actions have been added, or if
137
+ # any value cannot be represented as JSON
70
138
  def to_json
71
139
  raise InvalidManifestError, "at least one action is required" if @actions.empty?
72
140
 
73
141
  manifest = {
74
142
  "title" => @title,
143
+ "claim_generator_info" => [claim_generator_info],
75
144
  "assertions" => [
76
145
  { "label" => "c2pa.actions.v2", "data" => { "actions" => @actions } },
77
146
  *@assertions
@@ -79,7 +148,49 @@ module C2PA
79
148
  }
80
149
  manifest["ingredients"] = @ingredients unless @ingredients.empty?
81
150
 
82
- JSON.generate(manifest)
151
+ begin
152
+ JSON.generate(manifest)
153
+ rescue JSON::GeneratorError => e
154
+ # Typically a string that is not valid UTF-8 — a filename or caption
155
+ # read in another encoding and passed through untouched. Without this
156
+ # the caller gets a JSON::GeneratorError, which is not a C2PA::Error
157
+ # and so escapes `rescue C2PA::Error`.
158
+ raise InvalidManifestError,
159
+ "manifest contains text that cannot be encoded as JSON: #{e.message}"
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ # Parameters may be keyed with strings or symbols depending on the caller.
166
+ def param_present?(parameters, key)
167
+ return false unless parameters.is_a?(Hash)
168
+
169
+ !to_s_or_nil(parameters[key] || parameters[key.to_sym]).nil?
170
+ end
171
+
172
+ def to_s_or_nil(value)
173
+ return nil if value.nil?
174
+
175
+ string = value.to_s
176
+ string.empty? ? nil : string
177
+ end
178
+
179
+ # Who signed this. Without it c2pa-rs names itself, so every asset this gem
180
+ # produced credited "c2pa-rs" and nothing identified the gem or the
181
+ # application using it.
182
+ #
183
+ # c2pa-rs 0.78 permits exactly one entry — supplying two fails with "only 1
184
+ # claim_generator_info allowed" — so an application name replaces the gem
185
+ # rather than preceding it, and the gem moves into a namespaced field. That
186
+ # mirrors how c2pa-rs records itself as org.contentauth.c2pa_rs.
187
+ def claim_generator_info
188
+ return { "name" => "ruby-c2pa", "version" => VERSION } if @generator_name.nil?
189
+
190
+ info = { "name" => @generator_name }
191
+ info["version"] = @generator_version if @generator_version
192
+ info[GEM_FIELD] = VERSION
193
+ info
83
194
  end
84
195
  end
85
196
  end
data/lib/c2pa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module C2PA
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/c2pa.rb CHANGED
@@ -2,10 +2,18 @@ require "json"
2
2
  require_relative "c2pa/version"
3
3
  require_relative "c2pa/error"
4
4
  require_relative "c2pa/actions"
5
+ require_relative "c2pa/digital_source_types"
5
6
  require_relative "c2pa/manifest"
6
7
  require "c2pa/c2pa_native"
7
8
 
8
9
  module C2PA
10
+ # Validation states that mean a signed asset is good.
11
+ #
12
+ # "Trusted" is "Valid" plus a signing certificate that chains to a root in
13
+ # the active trust list. Accepting only "Valid" would reject exactly the
14
+ # production files that are most correct.
15
+ VALID_STATES = %w[Valid Trusted].freeze
16
+
9
17
  # Sign a file with a C2PA manifest.
10
18
  #
11
19
  # @param file [String] path to the input file
@@ -14,7 +22,10 @@ module C2PA
14
22
  # @param key [String] path to a PEM-encoded private key
15
23
  # @param algorithm [String] signing algorithm (default: "es256")
16
24
  # @param manifest [C2PA::Manifest] the manifest to embed
25
+ # @param verify [Boolean] read the signed file back and confirm it
26
+ # validates (default: true)
17
27
  # @return [String] the output path
28
+ # @raise [C2PA::SigningError] if signing fails, or if the signed file does not validate
18
29
  #
19
30
  # @example
20
31
  # manifest = C2PA::Manifest.new(title: "Sunset over the bay")
@@ -27,7 +38,7 @@ module C2PA
27
38
  # key: "key.pem",
28
39
  # manifest: manifest
29
40
  # )
30
- def self.sign(file:, output:, certificate:, key:, algorithm: "es256", manifest:)
41
+ def self.sign(file:, output:, certificate:, key:, algorithm: "es256", manifest:, verify: true)
31
42
  manifest_json = manifest.to_json
32
43
 
33
44
  raise SigningError, "Source file not found: '#{file}'" unless File.exist?(file)
@@ -35,9 +46,18 @@ module C2PA
35
46
  raise SigningError, "Key file not found: '#{key}'" unless File.exist?(key)
36
47
  raise SigningError, "Output file already exists: '#{output}'" if File.exist?(output)
37
48
 
38
- Native.sign_file(file, output, certificate, key, algorithm, manifest_json)
39
- rescue RuntimeError => e
40
- raise SigningError, e.message
49
+ begin
50
+ # to_json is the only thing genuinely required of a manifest, so an
51
+ # object that provides just that still signs — as a creation.
52
+ intent = manifest.respond_to?(:intent) ? manifest.intent&.to_s : nil
53
+ Native.sign_file(file, output, certificate, key, algorithm, manifest_json, intent)
54
+ rescue RuntimeError => e
55
+ raise SigningError, e.message
56
+ end
57
+
58
+ verify_signed_output!(output) if verify
59
+
60
+ output
41
61
  end
42
62
 
43
63
  # Read the C2PA manifest embedded in a signed file.
@@ -62,4 +82,48 @@ module C2PA
62
82
  def self.sdk_version
63
83
  Native.sdk_version
64
84
  end
85
+
86
+ # Read a freshly signed file back and confirm it actually validates.
87
+ #
88
+ # Signing reports success for manifests that verifiers reject. c2pa-rs
89
+ # applies its rules when reading, not when writing, so without this the gem
90
+ # hands back a path to a file that fails everywhere else — which is what
91
+ # released versions did, for years, silently.
92
+ #
93
+ # The rejected file is deleted rather than left behind, so a failed sign
94
+ # cannot leave something shippable on disk.
95
+ #
96
+ # @param output [String] path to the signed file
97
+ # @raise [C2PA::SigningError] if the file does not validate
98
+ def self.verify_signed_output!(output)
99
+ result =
100
+ begin
101
+ read(file: output)
102
+ rescue ReadError => e
103
+ discard(output)
104
+ raise SigningError, "signed file failed verification: could not read it back: #{e.message}"
105
+ end
106
+
107
+ state = result["validation_state"]
108
+ return if VALID_STATES.include?(state)
109
+
110
+ failures = Array(result.dig("validation_results", "activeManifest", "failure"))
111
+ .map { |failure| "#{failure["code"]} (#{failure["explanation"]})" }
112
+ detail = failures.empty? ? "no failure detail reported" : failures.uniq.join(", ")
113
+
114
+ discard(output)
115
+ raise SigningError,
116
+ "signed file failed verification: validation_state=#{state.inspect}, #{detail}. " \
117
+ "The output file has been removed. Pass verify: false to keep it for inspection."
118
+ end
119
+ private_class_method :verify_signed_output!
120
+
121
+ # Remove a file this library created and is about to reject.
122
+ def self.discard(path)
123
+ File.delete(path) if File.exist?(path)
124
+ rescue SystemCallError
125
+ # Leaving the file behind is better than masking the original failure.
126
+ nil
127
+ end
128
+ private_class_method :discard
65
129
  end
data/ruby-c2pa.gemspec CHANGED
@@ -8,10 +8,35 @@ Gem::Specification.new do |spec|
8
8
  spec.summary = "Ruby bindings for the c2pa content authenticity library"
9
9
  spec.description = "Embed and verify C2PA content provenance and authenticity credentials in images, video, and audio files. Ruby bindings for the official Rust c2pa-rs library."
10
10
  spec.license = "MIT"
11
- spec.homepage = "https://github.com/carlosrodriguez/ruby-c2pa"
12
- spec.metadata["source_code_uri"] = spec.homepage
11
+ spec.homepage = "https://github.com/eddorre/ruby-c2pa"
13
12
 
14
- spec.files = Dir["lib/**/*.rb", "ext/**/*.{rs,toml,rb}", "Rakefile", "*.gemspec", "LICENSE", "README.md"]
13
+ spec.metadata["source_code_uri"] = spec.homepage
14
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
15
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
16
+ spec.metadata["documentation_uri"] = "#{spec.homepage}/blob/main/README.md"
17
+
18
+ # Listed explicitly rather than globbed. "ext/**/*.rs" swept in whatever
19
+ # happened to be under ext/c2pa_native/target, so the package contents
20
+ # depended on what had been compiled on the machine that ran `gem build` —
21
+ # 1.3 MB of dependency build-script output shipped in 0.2.1.
22
+ #
23
+ # Cargo.lock is included deliberately. The extension is compiled at install
24
+ # time, so without it every installer resolves the dependency tree afresh.
25
+ # That is how 0.2.1 shipped against a broken atree and aborted the Ruby
26
+ # process on TIFF input.
27
+ spec.files = [
28
+ "LICENSE",
29
+ "README.md",
30
+ "CHANGELOG.md",
31
+ "CONTRIBUTING.md",
32
+ "Rakefile",
33
+ "ruby-c2pa.gemspec",
34
+ "ext/c2pa_native/Cargo.toml",
35
+ "ext/c2pa_native/Cargo.lock",
36
+ "ext/c2pa_native/extconf.rb",
37
+ *Dir["ext/c2pa_native/src/**/*.rs"],
38
+ *Dir["lib/**/*.rb"]
39
+ ].sort
15
40
  spec.require_paths = ["lib"]
16
41
  spec.extensions = ["ext/c2pa_native/extconf.rb"]
17
42
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-c2pa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Rodriguez
@@ -74,32 +74,30 @@ extensions:
74
74
  - ext/c2pa_native/extconf.rb
75
75
  extra_rdoc_files: []
76
76
  files:
77
+ - CHANGELOG.md
78
+ - CONTRIBUTING.md
77
79
  - LICENSE
78
80
  - README.md
79
81
  - Rakefile
82
+ - ext/c2pa_native/Cargo.lock
80
83
  - ext/c2pa_native/Cargo.toml
81
84
  - ext/c2pa_native/extconf.rb
82
85
  - ext/c2pa_native/src/lib.rs
83
- - ext/c2pa_native/target/release/build/oid-registry-c350c75504c969dc/out/oid_db.rs
84
- - ext/c2pa_native/target/release/build/pix-f303ab89d734032b/out/gamma_lut.rs
85
- - ext/c2pa_native/target/release/build/serde-2e9abb4d9d73cae4/out/private.rs
86
- - ext/c2pa_native/target/release/build/serde-f471616b462b0caf/out/private.rs
87
- - ext/c2pa_native/target/release/build/serde_core-2b94e9134dc44065/out/private.rs
88
- - ext/c2pa_native/target/release/build/serde_core-3dc945fa0ab21dd5/out/private.rs
89
- - ext/c2pa_native/target/release/build/thiserror-a1f4c63469c326b9/out/private.rs
90
- - ext/c2pa_native/target/release/build/typenum-4eb7d34b9fb695ef/out/tests.rs
91
- - ext/c2pa_native/target/release/build/typenum-b8de96984639a942/out/tests.rs
92
86
  - lib/c2pa.rb
93
87
  - lib/c2pa/actions.rb
88
+ - lib/c2pa/digital_source_types.rb
94
89
  - lib/c2pa/error.rb
95
90
  - lib/c2pa/manifest.rb
96
91
  - lib/c2pa/version.rb
97
92
  - ruby-c2pa.gemspec
98
- homepage: https://github.com/carlosrodriguez/ruby-c2pa
93
+ homepage: https://github.com/eddorre/ruby-c2pa
99
94
  licenses:
100
95
  - MIT
101
96
  metadata:
102
- source_code_uri: https://github.com/carlosrodriguez/ruby-c2pa
97
+ source_code_uri: https://github.com/eddorre/ruby-c2pa
98
+ bug_tracker_uri: https://github.com/eddorre/ruby-c2pa/issues
99
+ changelog_uri: https://github.com/eddorre/ruby-c2pa/blob/main/CHANGELOG.md
100
+ documentation_uri: https://github.com/eddorre/ruby-c2pa/blob/main/README.md
103
101
  rdoc_options: []
104
102
  require_paths:
105
103
  - lib