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

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