Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-06
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Context

`ssri::Integrity::from_str("")` does not return an error — it silently produces an `Integrity` value with an empty hash list. The existing `deserialize_sri` function only guards against `None` and parse errors, so an empty string passes through as `Some(Integrity{hashes: []})`. When `verify_checksum` calls `i.check(...)` on that value, `ssri` panics with an index-out-of-bounds.

The affected code path:
1. Live platform API returns `"integrity": ""` for newer Unity versions
2. `deserialize_sri` calls `Integrity::from_str("")` → succeeds, zero hashes
3. `verify_checksum` receives `Some(empty_integrity)`, calls `i.check(&bytes)` → panic

## Goals / Non-Goals

**Goals:**
- Treat `""` as absent integrity (same as `null`) in `deserialize_sri`

**Non-Goals:**
- Changing checksum verification logic
- Handling other malformed integrity values differently than today

## Decisions

### Guard empty string before `from_str`

Add `if s.is_empty() { return Ok(None); }` in `deserialize_sri` before the `Integrity::from_str` call. This is the earliest, safest point to intercept the bad value — it requires no changes to `verify_checksum` or callers, and keeps the existing `Err(_) => Ok(None)` fallback for other malformed values.

**Alternative considered**: Validate the parsed `Integrity` after construction (e.g., check `hashes.is_empty()`). Rejected — requires knowledge of `ssri` internals and is more fragile than rejecting the empty input directly.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Why

Newer Unity versions return an empty string (`""`) for the `integrity` field in the live platform API instead of `null`. `ssri::Integrity::from_str("")` parses successfully and produces an `Integrity` with zero hashes. When `verify_checksum` later calls `i.check(...)` on that value, `ssri` panics with an index-out-of-bounds error because it assumes at least one hash is present. This crash prevents installation of any Unity version that returns an empty integrity string.

## What Changes

- Treat an empty `integrity` string as absent (`None`) in `deserialize_sri`, so that `verify_checksum` receives `None` and returns `CheckSumResult::NoCheckSum` instead of panicking

## Capabilities

### New Capabilities
<!-- none -->

### Modified Capabilities
- `payload-format-detection`: the deserialization of the `integrity` field must now handle empty-string values in addition to `null` and valid SRI strings

## Impact

- **`uvm_live_platform/src/model/release.rs`**: one-line guard in `deserialize_sri` — skip `from_str` and return `Ok(None)` when the string is empty
- No behaviour change for `null` or valid integrity values; installations without a checksum continue to be skipped gracefully
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## ADDED Requirements

### Requirement: Empty integrity string is treated as absent
The `integrity` field deserializer SHALL treat an empty string (`""`) as equivalent to `null`, producing `None` rather than a parsed `Integrity` value with zero hashes.

#### Scenario: Empty string produces None
- **WHEN** the live platform API returns `"integrity": ""`
- **THEN** the deserialized `integrity` field SHALL be `None`

#### Scenario: Checksum verification is skipped for empty integrity
- **WHEN** the `integrity` field deserializes to `None` due to an empty string
- **THEN** `verify_checksum` SHALL return `CheckSumResult::NoCheckSum` without panicking

#### Scenario: Null integrity still produces None
- **WHEN** the live platform API returns `"integrity": null`
- **THEN** the deserialized `integrity` field SHALL be `None` (existing behaviour preserved)

#### Scenario: Valid integrity string still parses correctly
- **WHEN** the live platform API returns a valid SRI string (e.g. `"sha256-abc123..."`)
- **THEN** the deserialized `integrity` field SHALL be `Some(Integrity)` (existing behaviour preserved)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## 1. Fix

- [x] 1.1 In `uvm_live_platform/src/model/release.rs`, add an early return in `deserialize_sri`: if the deserialized string is empty, return `Ok(None)` before calling `Integrity::from_str`

## 2. Tests

- [x] 2.1 Add a `#[cfg(test)]` module to `uvm_live_platform/src/model/release.rs` with unit tests for `deserialize_sri`:
- `empty_string_integrity_deserializes_to_none` — deserialize `{"integrity": ""}` into `UnityReleaseFile`, assert `integrity` is `None`
- `null_integrity_deserializes_to_none` — deserialize `{"integrity": null}`, assert `integrity` is `None`
- `missing_integrity_field_deserializes_to_none` — deserialize `{}` (field absent), assert `integrity` is `None`
- `valid_integrity_deserializes_to_some` — deserialize a valid SRI string, assert `integrity` is `Some`

## 3. Verify

- [x] 3.1 Run `cargo test -p uvm_live_platform` and confirm all tests pass
- [x] 3.2 Run `cargo clippy -p uvm_live_platform` and confirm no new warnings
20 changes: 20 additions & 0 deletions openspec/specs/integrity-deserialization/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Requirements

### Requirement: Empty integrity string is treated as absent
The `integrity` field deserializer SHALL treat an empty string (`""`) as equivalent to `null`, producing `None` rather than a parsed `Integrity` value with zero hashes.

#### Scenario: Empty string produces None
- **WHEN** the live platform API returns `"integrity": ""`
- **THEN** the deserialized `integrity` field SHALL be `None`

#### Scenario: Checksum verification is skipped for empty integrity
- **WHEN** the `integrity` field deserializes to `None` due to an empty string
- **THEN** `verify_checksum` SHALL return `CheckSumResult::NoCheckSum` without panicking

#### Scenario: Null integrity still produces None
- **WHEN** the live platform API returns `"integrity": null`
- **THEN** the deserialized `integrity` field SHALL be `None` (existing behaviour preserved)

#### Scenario: Valid integrity string still parses correctly
- **WHEN** the live platform API returns a valid SRI string (e.g. `"sha256-abc123..."`)
- **THEN** the deserialized `integrity` field SHALL be `Some(Integrity)` (existing behaviour preserved)
37 changes: 37 additions & 0 deletions uvm_live_platform/src/model/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ where
let sri_str: Option<String> = Option::deserialize(deserializer)?;

match sri_str {
Some(s) if s.is_empty() => Ok(None),
Some(s) => match Integrity::from_str(&s) {
Ok(integrity) => Ok(Some(integrity)),
Err(_) => Ok(None), // If parsing fails (e.g., MD5 hash), ignore it
Expand Down Expand Up @@ -165,3 +166,39 @@ impl<'a> Iterator for ModuleIterator<'a> {
Some(next)
}
}

#[cfg(test)]
mod tests {
use super::*;

fn deserialize_integrity(json: &str) -> Option<Integrity> {
let file: UnityReleaseFile = serde_json::from_str(json).unwrap();
file.integrity
}

#[test]
fn empty_string_integrity_deserializes_to_none() {
let result = deserialize_integrity(r#"{"integrity": ""}"#);
assert!(result.is_none());
}

#[test]
fn null_integrity_deserializes_to_none() {
let result = deserialize_integrity(r#"{"integrity": null}"#);
assert!(result.is_none());
}

#[test]
fn missing_integrity_field_deserializes_to_none() {
let result = deserialize_integrity(r#"{}"#);
assert!(result.is_none());
}

#[test]
fn valid_integrity_deserializes_to_some() {
let result = deserialize_integrity(
r#"{"integrity": "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="}"#,
);
assert!(result.is_some());
}
}
Loading