zon-rb 0.2.0 → 0.3.1

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: c0e8315346d015f85b7448071350fecfed55f786537ee0c9b7dd20d4890f5808
4
+ data.tar.gz: 4b06d71e623065c11255cdcff369bdbd9429a903083d26c922a6e6867ef79e2a
5
5
  SHA512:
6
- metadata.gz: a235b0b85a9d90cd4f7f1a132abb87609f5f7c5575922de790818072ebb1d84e63fee96a62eee3757e57bf6433ca8e9d7ffff037e8ffdc1259d8e582b1b2343b
7
- data.tar.gz: f5f160c938a43879d6b428cdb0d55ccead5ba9baaa9bf31362137dfe84f6e61a19021890d5bc9d1049e591dac9d55e39f0e1a0c4408a0bdf89ec31cac16b112a
6
+ metadata.gz: 18728d5a2095893cac9895e71039ec8503074d3b3600291f004d08998028d6bf32acd7f50a6e3aa74a4a82b07c1f94d1472040f41ff0b7d6ac01b11ece66fb1b
7
+ data.tar.gz: 7f8bf0654d20950c09c12efb9a761d00545abe672d30e5a54513bb0128fe3e07581ca6df6c6875332b64d887499778ed36f0b38d40c517e0367d4e7b1ed85563
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,174 @@
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
+ def initialize(package_path, manifest_name: "build.zig.zon")
37
+ # Make sure we have a valid path to work with
38
+ @package_path = package_path
39
+ raise ArgumentError, "not a directory '#{@package_path}'" if not Dir.exist? @package_path
40
+
41
+ manifest_path = File.join(@package_path, manifest_name)
42
+ raise ArgumentError, "no manifest '#{manifest_name}' in '#{@package_path}'" if not File.exist? manifest_path
43
+
44
+ @manifest = Zon.parse(File.open(manifest_path))
45
+ raise ArgumentError, "no 'paths' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :paths
46
+ raise ArgumentError, "no 'name' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :name
47
+ raise ArgumentError, "no 'version' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :version
48
+ raise ArgumentError, "no 'fingerprint' field in ZON manifest '#{manifest_path}'" if not @manifest.key? :fingerprint
49
+
50
+ @paths = []
51
+ @results = {}
52
+ @total_size = 0
53
+ @digest = nil
54
+
55
+ @manifest[:paths].each do |path|
56
+ full_path = File.join(@package_path, path)
57
+
58
+ if File.directory? full_path
59
+ f = Find.find(full_path)
60
+ f.each do |p|
61
+ next if File.directory? p
62
+
63
+ pstr = p.delete_prefix(@package_path)[1..]
64
+ @paths.append(pstr) if not @paths.include? pstr
65
+ end
66
+ elsif File.file? full_path or File.symlink? full_path
67
+ pstr = full_path.delete_prefix(@package_path)[1..]
68
+ @paths.append(pstr) if not @paths.include? pstr
69
+ end
70
+ end
71
+
72
+ @paths = @paths.sort_by { |str| [str, -str.length] }
73
+ end
74
+
75
+ def name
76
+ String(@manifest[:name])
77
+ end
78
+
79
+ def version
80
+ @manifest[:version]
81
+ end
82
+
83
+ def hash
84
+ @total_size = 0
85
+ @digest = nil
86
+
87
+ # Hash all files individually
88
+ @paths.each do |str|
89
+ @results[str] = Zon::Zig::Hasher::hash_file(@package_path, str)
90
+ end
91
+
92
+ sha2 = Digest::SHA2.new
93
+
94
+ @paths.each do |str|
95
+ res = @results[str]
96
+
97
+ @total_size += res[:size]
98
+ sha2.update [res[:digest]].pack('H*')
99
+ end
100
+
101
+ @digest = sha2.hexdigest
102
+ end
103
+
104
+ ##
105
+ # Get the ID part of the fingerprint
106
+ def get_id
107
+ @manifest[:fingerprint] & 0xffffffff
108
+ end
109
+
110
+ ##
111
+ # Get the calculated, total size of the unpacked package.
112
+ def get_saturated_size
113
+ max_uint32 = 2**32 - 1
114
+
115
+ if @total_size > max_uint32
116
+ max_uint32
117
+ else
118
+ @total_size
119
+ end
120
+ end
121
+
122
+ def result
123
+ Zon::Zig::Hasher::make_hash(@digest, name, version, get_id, get_saturated_size)
124
+ end
125
+
126
+ ##
127
+ # Produces $name-$semver-$hashplus
128
+ #
129
+ # - name is the name field from a build.zig.zon manifest. It is expected
130
+ # to be at most 32 bytes long and a valid Zig identifier.
131
+ # - semver is the version field from a build.zig.zon manifest. It is expected
132
+ # to be at most 32 bytes long.
133
+ # - hashplus is a base64 (urlsafe and nopad) encoded, 33-byte string:
134
+ # - Pacakge ID (4) || Decompressed Size (4) || Digest0, Digest1, ..., Digest24
135
+ def self.make_hash(digest, name, semver, id, size)
136
+ raise ArgumentError, "name '#{name}' is expected to be at most 32 bytes long but it is #{name.bytesize} bytes" if name.bytesize > 32
137
+ raise ArgumentError, "semver '#{semver}' is expected to be at most 32 bytes long but it is #{semver.bytesize} bytes" if semver.bytesize > 32
138
+
139
+ hash_plus = [id].pack("V")
140
+ hash_plus += [size].pack("V")
141
+ hash_plus += [digest].pack('H*')[0..24]
142
+
143
+ "#{name}-#{semver}-#{Base64.urlsafe_encode64(hash_plus, padding: false)}"
144
+ end
145
+
146
+ def self.hash_file(package_path, path)
147
+ sha2 = Digest::SHA2.new
148
+ sha2.update path
149
+
150
+ full_path = File.join(package_path, path)
151
+
152
+ if File.file? full_path
153
+ f = File.open full_path
154
+ data = f.read
155
+ file_size = data.bytesize
156
+
157
+ # Hard-coded false executable bit: https://github.com/ziglang/zig/issues/17463
158
+ sha2.update "\x00\x00"
159
+ sha2.update data
160
+ elsif File.symlink? full_path
161
+ link_name = File.readlink full_path
162
+ sha2.update link_name
163
+ end
164
+
165
+ # TODO: symlink
166
+
167
+ {
168
+ digest: sha2.hexdigest,
169
+ size: file_size,
170
+ }
171
+ end
172
+ end
173
+ end
174
+ 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.1"
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.1
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: 1980-01-02 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
@@ -52,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
53
  - !ruby/object:Gem::Version
53
54
  version: '0'
54
55
  requirements: []
55
- rubygems_version: 3.6.2
56
+ rubygems_version: 4.0.16
56
57
  specification_version: 4
57
58
  summary: Zig Object Notation (ZON) support for Ruby
58
59
  test_files: []