Module: Gemvault::ManifestText
- Defined in:
- lib/gemvault/manifest_text.rb
Overview
The manifest's on-disk notation: a header and one line per stored gem.
gemvault 3
created 2026-08-12T16:05:55Z
foo 1.0.0 ruby 2026-08-12T16:05:55Z <sha256> 0
A manifest is a table, not a document -- fixed-arity records of scalars, with no nesting and no free text -- so it is written and read as one. Every field comes from an alphabet that excludes whitespace: rubygems validates gem names against /\A[a-zA-Z0-9._-]+\z/, versions and platforms are drawn from the same characters, digests are hex, the flag is a bit, and Gemvault::Timestamp keeps times space-free. That makes a line unambiguous without quoting or escapes.
The gain over a general notation is what a reader cannot be asked to do: there is no recursion to exhaust the stack, no escape grammar, no backtracking, and no library to load -- the last of which is what let a require "json" in this path activate a gem version a project had locked (issue #25).
Reading validates every field, because a vault is a file that arrives from elsewhere. Writing trusts the values this library computed or normalized (digests, flags, times through Gemvault::Timestamp) -- but a gem's identity arrives in a gem file this library did not write, and Gem::Package#spec never validates it, so a vault asks unwritable_field before admitting a gem.
Defined Under Namespace
Classes: Header, MalformedError
Constant Summary collapse
- FILENAME =
The archive entry holding the manifest, and the one older gemvaults wrote, recognized only to say so.
"manifest".freeze
- LEGACY_FILENAME =
"manifest.json".freeze
- MAGIC =
"gemvault".freeze
- HEADER_LINES =
3- NAME =
Field alphabets, composed into RECORD below.
"[a-zA-Z0-9._-]+".freeze
- VERSION =
"[0-9][0-9a-zA-Z.-]*".freeze
- PLATFORM =
"[a-zA-Z0-9._-]+".freeze
- STAMP =
"\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ".freeze
- DIGEST =
"\\h{64}".freeze
- FLAG =
"[01]".freeze
- MAGIC_LINE =
gemvault
/\A#{MAGIC} (?<version>\d+)\z/- CREATED_LINE =
created
/\Acreated (?<created_at>#{STAMP})\z/- RECORD_FIELDS =
A record line's fields, in the order the line carries them.
{ name: NAME, version: VERSION, platform: PLATFORM, stored_at: STAMP, sha256: DIGEST, encrypted: FLAG }.freeze
- RECORD =
/\A#{RECORD_FIELDS.map { |field, alphabet| "(?<#{field}>#{alphabet})" }.join(" ")}\z/- IDENTITY_ALPHABETS =
The spec-supplied alphabets anchored singly, for asking whether one field fits before a record is written.
{ name: /\A#{NAME}\z/, version: /\A#{VERSION}\z/, platform: /\A#{PLATFORM}\z/ }.freeze
- ENCRYPTED =
"1".freeze
Class Method Summary collapse
-
.parse(text) ⇒ Object
:call-seq: parse(text) -> Manifest.
-
.render(manifest) ⇒ Object
:call-seq: render(manifest) -> String.
-
.unwritable_field(entry) ⇒ Object
:call-seq: unwritable_field(entry) -> Symbol or nil.
Class Method Details
.parse(text) ⇒ Object
:call-seq:
parse(text) -> Manifest
The Manifest text describes. Raises MalformedError for anything outside
the notation above; the version a well-formed header declares is returned
as parsed, readability being Vault.assert_readable!'s question.
108 109 110 111 112 113 |
# File 'lib/gemvault/manifest_text.rb', line 108 def parse(text) lines = text.to_s.lines(chomp: true) header = read_header(lines) records = lines.drop(HEADER_LINES).map { |line| read_record(line) } Manifest.new(created_at: header.created_at, records:, format_version: header.format_version) end |
.render(manifest) ⇒ Object
:call-seq:
render(manifest) -> String
manifest as the text a vault stores.
82 83 84 85 |
# File 'lib/gemvault/manifest_text.rb', line 82 def render(manifest) header = Header.new(format_version: Manifest::FORMAT_VERSION, created_at: manifest.created_at) "#{(header.lines + manifest.records.map { |record| record_line(record) }).join("\n")}\n" end |
.unwritable_field(entry) ⇒ Object
:call-seq:
unwritable_field(entry) -> Symbol or nil
The first of +entry+'s identity fields holding a value outside its
alphabet, or nil when a record for entry would read back intact.
98 99 100 |
# File 'lib/gemvault/manifest_text.rb', line 98 def unwritable_field(entry) IDENTITY_ALPHABETS.each_key.find { |field| !IDENTITY_ALPHABETS[field].match?(entry.public_send(field)) } end |