-
Notifications
You must be signed in to change notification settings - Fork 7
feat: Settings building and validation #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| name: Check C2PA schema changes | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "30 */6 * * *" | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| check-schema: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
|
|
||
| - name: Get current and previous c2pa-rs tags | ||
| id: get_tags | ||
| run: | | ||
| TAGS=$(curl -s "https://api.github.com/repos/contentauth/c2pa-rs/releases" | \ | ||
| jq -r '.[].tag_name | select(test("^c2pa-v"))' | \ | ||
| head -2) | ||
| CURRENT_TAG=$(echo "$TAGS" | head -1) | ||
| PREVIOUS_TAG=$(echo "$TAGS" | tail -1) | ||
| if [ -z "$CURRENT_TAG" ] || [ -z "$PREVIOUS_TAG" ]; then | ||
| echo "Could not find two consecutive c2pa release tags" | ||
| exit 1 | ||
| fi | ||
| if [ "$CURRENT_TAG" = "$PREVIOUS_TAG" ]; then | ||
| echo "Only one tag found, nothing to compare" | ||
| exit 1 | ||
| fi | ||
| echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT | ||
| echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Clone c2pa-rs | ||
| run: | | ||
| git clone https://github.com/contentauth/c2pa-rs.git /tmp/c2pa-rs | ||
| cd /tmp/c2pa-rs | ||
| git fetch --tags | ||
|
|
||
| - name: Generate schemas for both tags | ||
| run: | | ||
| for TAG_VAR in current previous; do | ||
| if [ "$TAG_VAR" = "current" ]; then | ||
| TAG="${{ steps.get_tags.outputs.current_tag }}" | ||
| else | ||
| TAG="${{ steps.get_tags.outputs.previous_tag }}" | ||
| fi | ||
| cd /tmp/c2pa-rs | ||
| git checkout "$TAG" | ||
| cargo run -p export_schema --features c2pa/rust_native_crypto 2>/dev/null || \ | ||
| cargo run -p export_schema 2>/dev/null | ||
| mkdir -p "/tmp/schemas-$TAG_VAR" | ||
| cp target/schema/*.schema.json "/tmp/schemas-$TAG_VAR/" | ||
| done | ||
|
|
||
| - name: Compare schemas | ||
| id: compare | ||
| run: | | ||
| CHANGES="" | ||
| HAS_CHANGES=false | ||
|
|
||
| for schema in /tmp/schemas-current/*.schema.json; do | ||
| NAME=$(basename "$schema") | ||
| PREV="/tmp/schemas-previous/$NAME" | ||
|
|
||
| if [ ! -f "$PREV" ]; then | ||
| CHANGES="${CHANGES}- **${NAME}**: New schema\n" | ||
| HAS_CHANGES=true | ||
| continue | ||
| fi | ||
|
|
||
| DIFF=$(diff \ | ||
| <(python3 -c "import json,sys; json.dump(json.load(open('$PREV')),sys.stdout,sort_keys=True,indent=2)") \ | ||
| <(python3 -c "import json,sys; json.dump(json.load(open('$schema')),sys.stdout,sort_keys=True,indent=2)") \ | ||
| || true) | ||
|
|
||
| if [ -n "$DIFF" ]; then | ||
| ADDED=$(echo "$DIFF" | grep -c '^>' || true) | ||
| REMOVED=$(echo "$DIFF" | grep -c '^<' || true) | ||
| CHANGES="${CHANGES}- **${NAME}**: ${ADDED} additions, ${REMOVED} removals\n" | ||
| HAS_CHANGES=true | ||
| fi | ||
| done | ||
|
|
||
| for schema in /tmp/schemas-previous/*.schema.json; do | ||
| NAME=$(basename "$schema") | ||
| if [ ! -f "/tmp/schemas-current/$NAME" ]; then | ||
| CHANGES="${CHANGES}- **${NAME}**: Schema removed\n" | ||
| HAS_CHANGES=true | ||
| fi | ||
| done | ||
|
|
||
| echo "has_changes=$HAS_CHANGES" >> $GITHUB_OUTPUT | ||
| if [ "$HAS_CHANGES" = true ]; then | ||
| echo "changes<<EOF" >> $GITHUB_OUTPUT | ||
| echo -e "$CHANGES" >> $GITHUB_OUTPUT | ||
| echo "EOF" >> $GITHUB_OUTPUT | ||
| fi | ||
|
|
||
| - name: Ensure schema-change label exists | ||
| if: steps.compare.outputs.has_changes == 'true' | ||
| run: | | ||
| gh label create "schema-change" \ | ||
| --description "Upstream c2pa-rs JSON schema has changed" \ | ||
| --color "D93F0B" \ | ||
| 2>/dev/null || true | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Check for existing open issue | ||
| if: steps.compare.outputs.has_changes == 'true' | ||
| id: existing_issue | ||
| run: | | ||
| EXISTING=$(gh issue list \ | ||
| --label "schema-change" \ | ||
| --state open \ | ||
| --json number \ | ||
| --jq '.[0].number // empty') | ||
| echo "number=$EXISTING" >> $GITHUB_OUTPUT | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: File issue for schema changes | ||
| if: steps.compare.outputs.has_changes == 'true' && steps.existing_issue.outputs.number == '' | ||
| run: | | ||
| gh issue create \ | ||
| --title "C2PA schema changes detected between ${{ steps.get_tags.outputs.previous_tag }} and ${{ steps.get_tags.outputs.current_tag }}" \ | ||
| --label "schema-change" \ | ||
| --body "$(cat <<'ISSUE_EOF' | ||
| ## Schema Changes Detected | ||
|
|
||
| Changes were detected in the upstream c2pa-rs JSON schemas between \ | ||
| `${{ steps.get_tags.outputs.previous_tag }}` and `${{ steps.get_tags.outputs.current_tag }}`. | ||
|
|
||
| ### Changed Schemas | ||
|
|
||
| ${{ steps.compare.outputs.changes }} | ||
|
|
||
| ### Action Required | ||
|
|
||
| Review the upstream schema changes and update `C2PASettingsDefinition` in \ | ||
| `Library/Sources/Manifest/SettingsDefinition.swift` to match the new schema. | ||
|
|
||
| See the [c2pa-rs releases](https://github.com/contentauth/c2pa-rs/releases) for details. | ||
| ISSUE_EOF | ||
| )" | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| // This file is licensed to you under the Apache License, Version 2.0 | ||
| // (http://www.apache.org/licenses/LICENSE-2.0) or the MIT license | ||
| // (http://opensource.org/licenses/MIT), at your option. | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, this software is | ||
| // distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF | ||
| // ANY KIND, either express or implied. See the LICENSE-MIT and LICENSE-APACHE | ||
| // files for the specific language governing permissions and limitations under | ||
| // each license. | ||
| // | ||
| // C2PASettings.swift | ||
| // | ||
|
|
||
| import C2PAC | ||
| import Foundation | ||
|
|
||
| /// Manages C2PA settings configuration. | ||
| /// | ||
| /// `C2PASettings` provides a Swift-idiomatic interface for loading and applying | ||
| /// C2PA settings in JSON or TOML format. Settings control signer configuration, | ||
| /// CAWG identity assertions, thumbnail generation, and other build options. | ||
| /// | ||
| /// Settings can be loaded from raw JSON/TOML strings or from a type-safe | ||
| /// ``C2PASettingsDefinition`` struct. | ||
| /// | ||
| /// ## Example | ||
| /// | ||
| /// ```swift | ||
| /// // From a raw JSON string | ||
| /// let settings = try C2PASettings(json: settingsJSON) | ||
| /// | ||
| /// // From a type-safe definition | ||
| /// let definition = C2PASettingsDefinition( | ||
| /// version: 1, | ||
| /// signer: .local(LocalSignerSettings( | ||
| /// alg: "es256", | ||
| /// signCert: certPEM, | ||
| /// privateKey: keyPEM | ||
| /// )) | ||
| /// ) | ||
| /// let settings = try C2PASettings(definition: definition) | ||
| /// ``` | ||
| /// | ||
| /// - SeeAlso: ``Signer``, ``C2PASettingsDefinition`` | ||
| public final class C2PASettings { | ||
| private var settingsString: String | ||
| private var format: String | ||
|
|
||
| /// Creates settings from a JSON string. | ||
| /// | ||
| /// - Parameter json: A JSON string containing C2PA settings. | ||
| /// - Throws: ``C2PAError`` if the JSON is invalid. | ||
| public init(json: String) throws { | ||
| self.settingsString = json | ||
| self.format = "json" | ||
| try apply() | ||
| } | ||
|
|
||
| /// Creates settings from a TOML string. | ||
| /// | ||
| /// - Parameter toml: A TOML string containing C2PA settings. | ||
| /// - Throws: ``C2PAError`` if the TOML is invalid. | ||
| public init(toml: String) throws { | ||
| self.settingsString = toml | ||
| self.format = "toml" | ||
| try apply() | ||
| } | ||
|
|
||
| /// Creates settings from a type-safe ``C2PASettingsDefinition``. | ||
| /// | ||
| /// The definition is encoded to JSON and applied to the C2PA runtime. | ||
| /// | ||
| /// - Parameter definition: A settings definition struct. | ||
| /// - Throws: ``C2PAError`` if the settings are invalid. | ||
| /// | ||
| /// - SeeAlso: ``C2PASettingsDefinition`` | ||
| public init(definition: C2PASettingsDefinition) throws { | ||
| self.settingsString = try C2PAJson.encode(definition) | ||
| self.format = "json" | ||
| try apply() | ||
| } | ||
|
|
||
| /// Loads additional JSON settings, merging with existing configuration. | ||
| /// | ||
| /// - Parameter json: A JSON string containing C2PA settings to merge. | ||
| /// - Throws: ``C2PAError`` if the JSON is invalid. | ||
| public func load(json: String) throws { | ||
| self.settingsString = json | ||
| self.format = "json" | ||
| try apply() | ||
| } | ||
|
|
||
| /// Loads additional TOML settings, merging with existing configuration. | ||
| /// | ||
| /// - Parameter toml: A TOML string containing C2PA settings to merge. | ||
| /// - Throws: ``C2PAError`` if the TOML is invalid. | ||
| public func load(toml: String) throws { | ||
| self.settingsString = toml | ||
| self.format = "toml" | ||
| try apply() | ||
| } | ||
|
|
||
| /// Loads settings from a type-safe ``C2PASettingsDefinition``, | ||
| /// merging with existing configuration. | ||
| /// | ||
| /// - Parameter definition: A settings definition struct. | ||
| /// - Throws: ``C2PAError`` if the settings are invalid. | ||
| public func load(definition: C2PASettingsDefinition) throws { | ||
| self.settingsString = try C2PAJson.encode(definition) | ||
| self.format = "json" | ||
| try apply() | ||
| } | ||
|
|
||
| /// Sets a single value at the given dot-separated path within the settings. | ||
| /// | ||
| /// This method parses the current JSON settings, navigates to the specified | ||
| /// path, sets the value, and re-applies the updated settings. | ||
| /// | ||
| /// - Parameters: | ||
| /// - value: The value to set. Must be a JSON-compatible type | ||
| /// (`String`, `Int`, `Double`, `Bool`, or `nil`). | ||
| /// - path: A dot-separated path (e.g., `"builder.thumbnail.format"`). | ||
| /// | ||
| /// - Throws: ``C2PAError`` if the format is not JSON or the path is invalid. | ||
| /// | ||
| /// ## Example | ||
| /// | ||
| /// ```swift | ||
| /// let settings = try C2PASettings(json: "{\"version\": 1}") | ||
| /// try settings.setValue("es256", forPath: "signer.local.alg") | ||
| /// ``` | ||
| public func setValue(_ value: Any, forPath path: String) throws { | ||
| guard format == "json" else { | ||
| throw C2PAError.api("setValue is only supported for JSON settings") | ||
| } | ||
|
|
||
| guard let data = settingsString.data(using: .utf8), | ||
| var json = try JSONSerialization.jsonObject(with: data) as? [String: Any] | ||
| else { | ||
| throw C2PAError.api("Current settings are not a valid JSON object") | ||
| } | ||
|
|
||
| let components = path.split(separator: ".").map(String.init) | ||
| guard !components.isEmpty else { | ||
| throw C2PAError.api("Path must not be empty") | ||
| } | ||
|
|
||
| setNestedValue(&json, components: components, value: value) | ||
|
|
||
| let updatedData = try JSONSerialization.data(withJSONObject: json) | ||
| guard let updatedString = String(data: updatedData, encoding: .utf8) else { | ||
| throw C2PAError.utf8 | ||
| } | ||
|
|
||
| self.settingsString = updatedString | ||
| try apply() | ||
| } | ||
|
|
||
| /// Creates a ``Signer`` from the loaded settings. | ||
| /// | ||
| /// - Returns: A configured ``Signer`` instance. | ||
| /// - Throws: ``C2PAError`` if a signer cannot be created from the settings. | ||
| public func createSigner() throws -> Signer { | ||
| if format == "json" { | ||
| return try Signer(settingsJSON: settingsString) | ||
| } else { | ||
| return try Signer(settingsTOML: settingsString) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Private | ||
|
|
||
| private func apply() throws { | ||
| try settingsString.withCString { settingsPtr in | ||
| try format.withCString { formatPtr in | ||
| let result = c2pa_load_settings(settingsPtr, formatPtr) | ||
| guard result == 0 else { | ||
| throw C2PAError.api(lastC2PAError()) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func setNestedValue( | ||
| _ dict: inout [String: Any], | ||
| components: [String], | ||
| value: Any | ||
| ) { | ||
| guard let key = components.first else { return } | ||
|
|
||
| if components.count == 1 { | ||
| dict[key] = value | ||
| } else { | ||
| var nested = dict[key] as? [String: Any] ?? [:] | ||
| setNestedValue(&nested, components: Array(components.dropFirst()), value: value) | ||
| dict[key] = nested | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!