zon-rb 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2422c6a081bd6076413277434ed67b585057cba3858faf67b78e4f21efdfa74
4
- data.tar.gz: e139d7d3da59ab7e82b7daca9267fb6b94c17b3437afbfb7bc21e67ca798682a
3
+ metadata.gz: 818004dd00ec5169c64a80953c55e2b49f9b92a7691aef46251463e671c8a033
4
+ data.tar.gz: 2519a754dd2e2cc19ee40c8dbfa7c2ae0370e5bd33fc6e69f7dcc0bebd56eb00
5
5
  SHA512:
6
- metadata.gz: a235b0b85a9d90cd4f7f1a132abb87609f5f7c5575922de790818072ebb1d84e63fee96a62eee3757e57bf6433ca8e9d7ffff037e8ffdc1259d8e582b1b2343b
7
- data.tar.gz: f5f160c938a43879d6b428cdb0d55ccead5ba9baaa9bf31362137dfe84f6e61a19021890d5bc9d1049e591dac9d55e39f0e1a0c4408a0bdf89ec31cac16b112a
6
+ metadata.gz: e03c8137c996cb03adc77ddebc0afbb95a6cb078f635052eb88dfb2b782365e7ca335aa5436560619b20e246d7693ba96860625943b780deb15c1a2386ef9472
7
+ data.tar.gz: 497d4a8b6b830abe6b533fdbb22407524616f5b44727d7c73d8a0f9e49105a7db6d2941396c3a904e4dcfc029c0f5157eb4dfe1db63b22248032d56052403344
data/README.md CHANGED
@@ -114,6 +114,18 @@ manifest = Zon::Zig::Manifest.new o
114
114
  # ...
115
115
  ```
116
116
 
117
+ ### Zig Package Hash
118
+
119
+ Given a path `pdir` that points to the root of a Zig package, one can calculate the packages hash using the `Zon::Hasher`:
120
+
121
+ ```ruby
122
+ hasher = Zon::Zig::Hasher.new pdir
123
+ hasher.hash
124
+ # Hash with the format nnnn-vvvv-XXXX..XXXX
125
+ # e.g.: uuid-0.4.0-oOieIR2AAAChAUVBY4ABjYI1XN0EbVALmiN0JIlggC3i
126
+ result = hasher.result
127
+ ```
128
+
117
129
  ## Development
118
130
 
119
131
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/zon/hasher.rb ADDED
@@ -0,0 +1,176 @@
1
+ require "digest"
2
+ require "find"
3
+ require "base64"
4
+
5
+ module Zon
6
+ module Zig
7
+ ##
8
+ # Produces a package hash of the format: $name-$semver-$hashplus
9
+ #
10
+ # Along with 200 bits of a SHA-256, a package hash includes:
11
+ # - package name
12
+ # - package version
13
+ # - id component of the package fingerprint
14
+ # - total unpacked size on disk
15
+ #
16
+ # The following steps are taken to calculate the hash:
17
+ # 1. Read the list of included paths from the manifest, i.e. files that are part of the package.
18
+ # 2. Resolve the paths:
19
+ # - for directories: include all files that are within the directory or a child dir.
20
+ # - for files: include the file
21
+ # - for symlinks: TODO
22
+ # 3. Sort the paths by:
23
+ # 1. Lexographical order
24
+ # 2. by length
25
+ # 4. For every included file:
26
+ # - Calculate a SHA-256 over: RELATIVE_PATH_FROM_PKG_ROOT || 0x0000 || DATA
27
+ # - Record the file size in bytes
28
+ # - TODO: the 0x0000 will probably change in the future
29
+ # 5. Calculate a SHA-256 sum over all calculated file hashes
30
+ # - respecting the previously mentioned sorting rules.
31
+ # 6. Sum up the sizes of all files into a u32 (sizes that don't fit into a 32-bit unsingned integer will be saturated with the value 2**32 - 1)
32
+ # 7. Produce the package hash
33
+ class Hasher
34
+ attr_reader :paths, :total_size, :digest
35
+
36
+ SUPPORTED_ZIG_VERSIONS = ["0.14.0", "0.14.1", "0.15.1", "0.15.2"]
37
+
38
+ def initialize(package_path, manifest_name: "build.zig.zon")
39
+ # Make sure we have a valid path to work with
40
+ @package_path = package_path
41
+ raise ArgumentError, "not a directory '#{@package_path}'" if not Dir.exist? @package_path
42
+
43
+ manifest_path = File.join(@package_path, manifest_name)
44
+ raise ArgumentError, "no manifest '#{manifest_name}' in '#{@package_path}'" if not File.exist? manifest_path
45
+
46
+ @manifest = Zon.parse(File.open(manifest_path))
47
+ raise ArgumentError, "no 'paths' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :paths
48
+ raise ArgumentError, "no 'name' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :name
49
+ raise ArgumentError, "no 'version' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :version
50
+ raise ArgumentError, "no 'fingerprint' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :fingerprint
51
+
52
+ @paths = []
53
+ @results = {}
54
+ @total_size = 0
55
+ @digest = nil
56
+
57
+ @manifest[:paths].each do |path|
58
+ full_path = File.join(@package_path, path)
59
+
60
+ if File.directory? full_path
61
+ f = Find.find(full_path)
62
+ f.each do |p|
63
+ next if File.directory? p
64
+
65
+ pstr = p.delete_prefix(@package_path)[1..]
66
+ @paths.append(pstr) if not @paths.include? pstr
67
+ end
68
+ elsif File.file? full_path or File.symlink? full_path
69
+ pstr = full_path.delete_prefix(@package_path)[1..]
70
+ @paths.append(pstr) if not @paths.include? pstr
71
+ end
72
+ end
73
+
74
+ @paths = @paths.sort_by { |str| [str, -str.length] }
75
+ end
76
+
77
+ def name
78
+ String(@manifest[:name])
79
+ end
80
+
81
+ def version
82
+ @manifest[:version]
83
+ end
84
+
85
+ def hash
86
+ @total_size = 0
87
+ @digest = nil
88
+
89
+ # Hash all files individually
90
+ @paths.each do |str|
91
+ @results[str] = Zon::Zig::Hasher::hash_file(@package_path, str)
92
+ end
93
+
94
+ sha2 = Digest::SHA2.new
95
+
96
+ @paths.each do |str|
97
+ res = @results[str]
98
+
99
+ @total_size += res[:size]
100
+ sha2.update [res[:digest]].pack('H*')
101
+ end
102
+
103
+ @digest = sha2.hexdigest
104
+ end
105
+
106
+ ##
107
+ # Get the ID part of the fingerprint
108
+ def get_id
109
+ @manifest[:fingerprint] & 0xffffffff
110
+ end
111
+
112
+ ##
113
+ # Get the calculated, total size of the unpacked package.
114
+ def get_saturated_size
115
+ max_uint32 = 2**32 - 1
116
+
117
+ if @total_size > max_uint32
118
+ max_uint32
119
+ else
120
+ @total_size
121
+ end
122
+ end
123
+
124
+ def result
125
+ Zon::Zig::Hasher::make_hash(@digest, name, version, get_id, get_saturated_size)
126
+ end
127
+
128
+ ##
129
+ # Produces $name-$semver-$hashplus
130
+ #
131
+ # - name is the name field from a build.zig.zon manifest. It is expected
132
+ # to be at most 32 bytes long and a valid Zig identifier.
133
+ # - semver is the version field from a build.zig.zon manifest. It is expected
134
+ # to be at most 32 bytes long.
135
+ # - hashplus is a base64 (urlsafe and nopad) encoded, 33-byte string:
136
+ # - Pacakge ID (4) || Decompressed Size (4) || Digest0, Digest1, ..., Digest24
137
+ def self.make_hash(digest, name, semver, id, size)
138
+ raise ArgumentError, "name '#{name}' is expected to be at most 32 bytes long but it is #{name.bytesize} bytes" if name.bytesize > 32
139
+ raise ArgumentError, "semver '#{semver}' is expected to be at most 32 bytes long but it is #{semver.bytesize} bytes" if semver.bytesize > 32
140
+
141
+ hash_plus = [id].pack("V")
142
+ hash_plus += [size].pack("V")
143
+ hash_plus += [digest].pack('H*')[0..24]
144
+
145
+ "#{name}-#{semver}-#{Base64.urlsafe_encode64(hash_plus, padding: false)}"
146
+ end
147
+
148
+ def self.hash_file(package_path, path)
149
+ sha2 = Digest::SHA2.new
150
+ sha2.update path
151
+
152
+ full_path = File.join(package_path, path)
153
+
154
+ if File.file? full_path
155
+ f = File.open full_path
156
+ data = f.read
157
+ file_size = data.bytesize
158
+
159
+ # Hard-coded false executable bit: https://github.com/ziglang/zig/issues/17463
160
+ sha2.update "\x00\x00"
161
+ sha2.update data
162
+ elsif File.symlink? full_path
163
+ link_name = File.readlink full_path
164
+ sha2.update link_name
165
+ end
166
+
167
+ # TODO: symlink
168
+
169
+ {
170
+ digest: sha2.hexdigest,
171
+ size: file_size,
172
+ }
173
+ end
174
+ end
175
+ end
176
+ end
data/lib/zon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zon
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/zon/zig.rb CHANGED
@@ -16,6 +16,7 @@ module Zon
16
16
  raise "Missing '.name'" if not zon[:name]
17
17
  raise "Missing '.version'" if not zon[:version]
18
18
  raise "Missing '.fingerprint'" if not zon[:fingerprint]
19
+ raise "Missing '.paths'" if not zon[:paths]
19
20
 
20
21
  if zon[:dependencies]
21
22
  zon[:dependencies].each do |key, value|
data/lib/zon.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "zon/lexer"
5
5
  require_relative "zon/parser"
6
6
  require_relative "zon/serializer"
7
7
  require_relative "zon/zig"
8
+ require_relative "zon/hasher"
8
9
 
9
10
  ##
10
11
  # Zig Object Notation (ZON) de-/serializer.
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zon-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David P. Sugar
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-09-27 00:00:00.000000000 Z
10
+ date: 2025-10-19 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: This gem allows you to translate Zig Object Notation (ZON) data into
13
13
  Ruby objects and vice versa.
@@ -24,6 +24,7 @@ files:
24
24
  - README.md
25
25
  - Rakefile
26
26
  - lib/zon.rb
27
+ - lib/zon/hasher.rb
27
28
  - lib/zon/lexer.rb
28
29
  - lib/zon/parser.rb
29
30
  - lib/zon/serializer.rb