zon-rb 0.1.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: 32686cf609ab53ad187e1ea5eab78453f396642b7a092c60d1104b843f7dd47c
4
- data.tar.gz: e022ce6c1cb840580a105acc23ace93bf42ebca5001af78d31d2bda183e18319
3
+ metadata.gz: 818004dd00ec5169c64a80953c55e2b49f9b92a7691aef46251463e671c8a033
4
+ data.tar.gz: 2519a754dd2e2cc19ee40c8dbfa7c2ae0370e5bd33fc6e69f7dcc0bebd56eb00
5
5
  SHA512:
6
- metadata.gz: 13629fb58006c1ca81cecce5d3bf893f96835e3d927458de00745b0f0c3ccfea40a640d75bfb783c76433be2bcc5b83488c1576325237134f561d286f224e5c5
7
- data.tar.gz: c0968e432df9bb625d9d6d49ad6104b6a0a9d9730180d7ef62a1efcb800e6405996184e39918a63ca45e6abae11b0834854ec365be8f90efc9949349bc7062d0
6
+ metadata.gz: e03c8137c996cb03adc77ddebc0afbb95a6cb078f635052eb88dfb2b782365e7ca335aa5436560619b20e246d7693ba96860625943b780deb15c1a2386ef9472
7
+ data.tar.gz: 497d4a8b6b830abe6b533fdbb22407524616f5b44727d7c73d8a0f9e49105a7db6d2941396c3a904e4dcfc029c0f5157eb4dfe1db63b22248032d56052403344
data/CHANGELOG.md CHANGED
@@ -1,4 +1,6 @@
1
- ## [Unreleased]
1
+ ## [0.2.0] - 2025-09-27
2
+
3
+ - Added basic Zig package manifest support.
2
4
 
3
5
  ## [0.1.0] - 2025-09-27
4
6
 
data/README.md CHANGED
@@ -1,25 +1,31 @@
1
1
  # Zig Object Notation (ZON) for Ruby
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/zon-rb.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/zon-rb)
4
+
3
5
  The [Zig](https://ziglang.org/) Object Notation (ZON) is a file format primarily used within the Zig ecosystem. For example, ZON is used for the Zig package [manifest](https://github.com/ziglang/zig/blob/b7ab62540963d80f68d0e9ee7ce18520fb173487/doc/build.zig.zon.md).
4
6
 
5
7
  ## Installation
6
8
 
7
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
8
-
9
9
  Install the gem and add to the application's Gemfile by executing:
10
10
 
11
11
  ```bash
12
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
12
+ bundle add zon-rb
13
13
  ```
14
14
 
15
15
  If bundler is not being used to manage dependencies, install the gem by executing:
16
16
 
17
17
  ```bash
18
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
18
+ gem install zon-rb
19
19
  ```
20
20
 
21
21
  ## Usage
22
22
 
23
+ To use this Gem you have to require it:
24
+
25
+ ```ruby
26
+ require "zon"
27
+ ```
28
+
23
29
  To parse ZON data into a Ruby object, one can use the `Zon::parse` method. The method expects either a `String` or `IO` (`File`) object as its argument.
24
30
 
25
31
  ```irb
@@ -94,6 +100,32 @@ irb(main):010> Zon::serialize(255, {:ibase => 2})
94
100
  => "0b11111111"
95
101
  ```
96
102
 
103
+ ### Zig Manifest
104
+
105
+ `Zon::Zig::Manifest` provides you with an abstraction for your `build.zig.zon`. It will also validate your Zig package [manifest](https://github.com/ziglang/zig/blob/b7ab62540963d80f68d0e9ee7ce18520fb173487/doc/build.zig.zon.md) and raise an exception if one of the required fields is missing.
106
+
107
+ ```ruby
108
+ o = Zon::parse(File.open("../PassKeeZ/build.zig.zon"))
109
+ manifest = Zon::Zig::Manifest.new o
110
+
111
+ # Use:
112
+ # manifest.name
113
+ # manifest.version
114
+ # ...
115
+ ```
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
+
97
129
  ## Development
98
130
 
99
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.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/zon/zig.rb ADDED
@@ -0,0 +1,80 @@
1
+ module Zon
2
+
3
+ ##
4
+ # Everything related specifically to the Zig programming language.
5
+ module Zig
6
+
7
+ ##
8
+ # An abstraction of the Zig package manifest.
9
+ #
10
+ # Every Zig package should have a +build.zig.zon+ in
11
+ # its package root, the manifest. Like a manifest in
12
+ # Ruby, it is used to describe the package and specify
13
+ # required dependencies.
14
+ class Manifest
15
+ def initialize(zon)
16
+ raise "Missing '.name'" if not zon[:name]
17
+ raise "Missing '.version'" if not zon[:version]
18
+ raise "Missing '.fingerprint'" if not zon[:fingerprint]
19
+ raise "Missing '.paths'" if not zon[:paths]
20
+
21
+ if zon[:dependencies]
22
+ zon[:dependencies].each do |key, value|
23
+ if value[:url]
24
+ raise "Missing '.hash' for dependency '#{key}'" if not value[:hash]
25
+ elsif not value[:path]
26
+ raise "Expected either '.url' or '.path' for dependency '#{key}'" if not value[:hash]
27
+ end
28
+ end
29
+ end
30
+
31
+ @zon = zon
32
+ end
33
+
34
+ ##
35
+ # Get the '.name' field of the manifest.
36
+ def name
37
+ @zon[:name]
38
+ end
39
+
40
+ ##
41
+ # Get the '.version' field of the manifest.
42
+ def version
43
+ @zon[:version]
44
+ end
45
+
46
+ ##
47
+ # Get the '.fingerprint' field of the manifest.
48
+ def fingerprint
49
+ @zon[:fingerprint]
50
+ end
51
+
52
+ ##
53
+ # Get the '.dependencies' field of the manifest.
54
+ #
55
+ # This will return nil if the '.dependencies' field does not exist.
56
+ def dependencies
57
+ @zon[:dependencies]
58
+ end
59
+
60
+ ##
61
+ # Get the '.paths' field of the manifest.
62
+ #
63
+ # This will return nil if the '.paths' field does not exist.
64
+ def paths
65
+ @zon[:paths]
66
+ end
67
+
68
+ ##
69
+ # Get the '.minimum_zig_version' field of the manifest.
70
+ #
71
+ # This will return nil if the '.minimum_zig_version' field does not exist.
72
+ def minimum_zig_version
73
+ @zon[:minimum_zig_version]
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ end
data/lib/zon.rb CHANGED
@@ -4,6 +4,8 @@ require_relative "zon/version"
4
4
  require_relative "zon/lexer"
5
5
  require_relative "zon/parser"
6
6
  require_relative "zon/serializer"
7
+ require_relative "zon/zig"
8
+ require_relative "zon/hasher"
7
9
 
8
10
  ##
9
11
  # Zig Object Notation (ZON) de-/serializer.
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zon-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.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
- description: This gem allows you to translate ZON data into Ruby objects and vice
13
- versa.
12
+ description: This gem allows you to translate Zig Object Notation (ZON) data into
13
+ Ruby objects and vice versa.
14
14
  email:
15
15
  - david@thesugar.de
16
16
  executables: []
@@ -24,10 +24,12 @@ 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
30
31
  - lib/zon/version.rb
32
+ - lib/zon/zig.rb
31
33
  - lib/zon/zon_grammar.treetop
32
34
  - sig/zon.rbs
33
35
  homepage: https://github.com/r4gus/zon-rb