Class: Kotoshu::Integrity::Manifest

Inherits:
Object
  • Object
show all
Defined in:
lib/kotoshu/integrity/manifest.rb

Overview

Parsed view of a content repo’s ‘manifest.json`.

Format (per TODO.impl/09-integrity-security.md task 1):

{
  "version": 1,
  "generated_at": "2026-06-25T10:00:00Z",
  "resources": {
    "en/spelling/index.dic": {
      "size": 49568,
      "sha256": "ab12...",
      "language": "en",
      "type": "spelling",
      "license": "LGPL/MPL/GPL",
      "source": "SCROLL"
    }
  }
}

Construction:

manifest = Manifest.parse(json_string)
manifest.fetch("en/spelling/index.dic")  # => Entry or nil
manifest.verify_content!("en/spelling/index.dic", bytes) # raises on mismatch

‘Manifest.load(url, http:)`, returns nil when the manifest 404s (graceful degradation — see module docs).

Defined Under Namespace

Classes: Entry

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entries, version: nil, generated_at: nil) ⇒ Manifest

Returns a new instance of Manifest.



82
83
84
85
86
# File 'lib/kotoshu/integrity/manifest.rb', line 82

def initialize(entries, version: nil, generated_at: nil)
  @entries = entries
  @version = version
  @generated_at = generated_at
end

Instance Attribute Details

#entriesObject (readonly)

Returns the value of attribute entries.



80
81
82
# File 'lib/kotoshu/integrity/manifest.rb', line 80

def entries
  @entries
end

#generated_atObject (readonly)

Returns the value of attribute generated_at.



80
81
82
# File 'lib/kotoshu/integrity/manifest.rb', line 80

def generated_at
  @generated_at
end

#versionObject (readonly)

Returns the value of attribute version.



80
81
82
# File 'lib/kotoshu/integrity/manifest.rb', line 80

def version
  @version
end

Class Method Details

.load(url, http: Kotoshu::Integrity::NetHTTP) ⇒ Object

Fetch and parse a manifest from a URL. Returns nil when the manifest is absent (HTTP 404/410) so callers can fall back to unverified downloads — see module docs. Any other failure (5xx, network error, parse error) raises.



73
74
75
76
77
78
# File 'lib/kotoshu/integrity/manifest.rb', line 73

def self.load(url, http: Kotoshu::Integrity::NetHTTP)
  body = http.get(url)
  return nil if body.nil?

  parse(body)
end

.parse(json) ⇒ Object

Parse a manifest JSON string. Returns an empty Manifest if the JSON is parseable but has no resources (caller treats as “no constraints”).



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/kotoshu/integrity/manifest.rb', line 46

def self.parse(json)
  data = JSON.parse(json)
  entries = {}
  (data["resources"] || {}).each do |path, fields|
    entries[path] = Entry.new(
      path: path,
      sha256: fields["sha256"],
      size: fields["size"],
      language: fields["language"],
      type: fields["type"],
      license: fields["license"],
      source: fields["source"]
    )
  end
  new(entries, version: data["version"], generated_at: data["generated_at"])
rescue JSON::ParserError => e
  raise Kotoshu::IntegrityError.new(
    "manifest",
    expected: "<valid JSON>",
    actual: "<parse error: #{e.message}>"
  )
end

Instance Method Details

#empty?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/kotoshu/integrity/manifest.rb', line 92

def empty?
  @entries.empty?
end

#fetch(path) ⇒ Object



88
89
90
# File 'lib/kotoshu/integrity/manifest.rb', line 88

def fetch(path)
  @entries[path]
end

#verify_content!(path, content, url: nil) ⇒ Object

Verify that content for ‘path` matches the manifest entry. Raises Kotoshu::IntegrityError on mismatch. No-op when the manifest has no entry for `path` (returns nil — caller decides whether to treat absence as failure in strict mode).



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/kotoshu/integrity/manifest.rb', line 100

def verify_content!(path, content, url: nil)
  entry = @entries[path]
  return nil unless entry

  actual = Digest::SHA256.hexdigest(content)
  unless actual == entry.sha256
    raise Kotoshu::IntegrityError.new(
      path,
      expected: entry.sha256,
      actual: actual,
      url: url
    )
  end
  true
end