dry-validation-rust 0.1.0.pre5 → 0.1.0.pre6
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 +4 -4
- data/.gitignore +13 -0
- data/.markdownlint.yml +9 -0
- data/.rubocop.yml +124 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.yardopts +4 -0
- data/AGENTS.md +376 -0
- data/CHANGELOG.md +71 -0
- data/CODE_OF_CONDUCT.md +42 -0
- data/CONTRIBUTING.md +159 -0
- data/Cargo.lock +809 -0
- data/Cargo.toml +10 -0
- data/GOVERNANCE.md +68 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +132 -0
- data/NOTICE.md +5 -5
- data/README.md +206 -181
- data/Rakefile +350 -0
- data/SECURITY.md +98 -0
- data/SECURITY_AUDIT.md +33 -0
- data/SUMMARY.md +30 -0
- data/SUPPORT.md +67 -0
- data/book.toml +9 -0
- data/codecov.yml +11 -0
- data/compatibility.yml +453 -0
- data/deny.toml +7 -0
- data/dry-validation-rust.gemspec +21 -23
- data/ext/dry_validation_rust/Cargo.toml +7 -7
- data/ext/dry_validation_rust/build.rs +5 -0
- data/ext/dry_validation_rust/extconf.rb +12 -0
- data/ext/dry_validation_rust/fuzz/.gitignore +5 -0
- data/ext/dry_validation_rust/fuzz/Cargo.toml +19 -0
- data/ext/dry_validation_rust/fuzz/corpus/parse_plan/basic_params.json +1 -0
- data/ext/dry_validation_rust/fuzz/fuzz_targets/parse_plan.rs +9 -0
- data/lib/dry/validation/rust/path_trie.rb +15 -8
- data/lib/dry/validation/rust/schema/dsl.rb +8 -0
- data/lib/dry/validation/rust/schema/field_definition.rb +3 -1
- data/lib/dry/validation/rust/schema/predicate_block.rb +2 -1
- data/lib/dry/validation/rust/schema/ruby_type_processor.rb +38 -23
- data/lib/dry/validation/rust/version.rb +1 -1
- data/supply-chain/audits.toml +4 -0
- data/supply-chain/config.toml +368 -0
- data/supply-chain/imports.lock +4 -0
- data/support_matrix.yml +9 -0
- metadata +64 -25
- data/docs/ARCHITECTURE.md +0 -256
- data/docs/COMPATIBILITY.md +0 -198
- data/docs/FEASIBILITY.md +0 -207
- data/docs/SUPPORT_MATRIX.md +0 -66
- data/docs/VERIFICATION.md +0 -128
data/Rakefile
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
require 'rb_sys/extensiontask'
|
|
6
|
+
require 'rubygems/package_task'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
require 'tmpdir'
|
|
9
|
+
require 'yaml'
|
|
10
|
+
require 'zlib'
|
|
11
|
+
|
|
12
|
+
EXTENSION_DIR = File.expand_path('ext/dry_validation_rust', __dir__)
|
|
13
|
+
GEMSPEC_PATH = File.expand_path('dry-validation-rust.gemspec', __dir__)
|
|
14
|
+
CROSS_COMPILE_PLATFORMS = %w[x86_64-linux aarch64-linux x86_64-darwin arm64-darwin].freeze
|
|
15
|
+
PREDICATE_MANIFEST_PATH = File.expand_path('predicates.yml', __dir__)
|
|
16
|
+
GENERATED_RUBY_PREDICATES_PATH = File.expand_path('lib/dry/validation/rust/generated_predicates.rb', __dir__)
|
|
17
|
+
GENERATED_RUST_PREDICATES_PATH = File.expand_path('ext/dry_validation_rust/src/generated_predicates.rs', __dir__)
|
|
18
|
+
PACKAGE_REQUIRED_FILES = %w[
|
|
19
|
+
CHANGELOG.md
|
|
20
|
+
LICENSE
|
|
21
|
+
NOTICE.md
|
|
22
|
+
predicates.yml
|
|
23
|
+
README.md
|
|
24
|
+
ext/dry_validation_rust/Cargo.lock
|
|
25
|
+
ext/dry_validation_rust/Cargo.toml
|
|
26
|
+
ext/dry_validation_rust/extconf.rb
|
|
27
|
+
ext/dry_validation_rust/src/lib.rs
|
|
28
|
+
lib/dry/validation/rust.rb
|
|
29
|
+
lib/dry/validation/rust/contract.rb
|
|
30
|
+
lib/dry/validation/rust/native.rb
|
|
31
|
+
lib/dry/validation/rust/version.rb
|
|
32
|
+
rust-toolchain.toml
|
|
33
|
+
].freeze
|
|
34
|
+
PACKAGE_FORBIDDEN_PATTERNS = {
|
|
35
|
+
'secret or credential files' => %r{(^|/)(?:\.env(?:\.|$)|.*\.(?:pem|key|p12|pfx)|id_(?:rsa|dsa|ed25519)|master\.key|credentials\.ya?ml\.enc)\z}i,
|
|
36
|
+
'local build artifacts' => %r{\A(?:pkg|coverage|\.bundle|\.ruby-lsp)/|\Aext/dry_validation_rust/(?:target/|Makefile\z|mkmf\.log\z|native\.)|(?:\.gem|\.o|\.so|\.bundle|\.dylib|\.dll|\.log)\z},
|
|
37
|
+
'editor files' => %r{(^|/)(?:\.DS_Store|.*~|#.*#|\.#.*|.*\.sw[op])\z|(^|/)\.(?:idea|vscode)/},
|
|
38
|
+
'non-runtime project material' => %r{\A(?:benchmark|examples|docs/codex)/}
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
def package_file_list(gem_path)
|
|
42
|
+
data_tar_gz = nil
|
|
43
|
+
|
|
44
|
+
File.open(gem_path, 'rb') do |file|
|
|
45
|
+
Gem::Package::TarReader.new(file) do |gem_tar|
|
|
46
|
+
gem_tar.each do |entry|
|
|
47
|
+
data_tar_gz = entry.read if entry.full_name == 'data.tar.gz'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
raise "Gem data archive missing from #{gem_path}" unless data_tar_gz
|
|
53
|
+
|
|
54
|
+
files = []
|
|
55
|
+
Zlib::GzipReader.wrap(StringIO.new(data_tar_gz)) do |gzip|
|
|
56
|
+
Gem::Package::TarReader.new(gzip) do |data_tar|
|
|
57
|
+
data_tar.each do |entry|
|
|
58
|
+
files << entry.full_name unless entry.directory?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
files.sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_package_files(gem_path, expected_files)
|
|
66
|
+
files = package_file_list(gem_path)
|
|
67
|
+
|
|
68
|
+
puts 'Package contents:'
|
|
69
|
+
puts(files.map { |path| " #{path}" })
|
|
70
|
+
|
|
71
|
+
missing = PACKAGE_REQUIRED_FILES - files
|
|
72
|
+
raise "Package is missing required files: #{missing.join(', ')}" unless missing.empty?
|
|
73
|
+
|
|
74
|
+
unexpected = files - expected_files
|
|
75
|
+
raise "Package contains files outside spec.files: #{unexpected.join(', ')}" unless unexpected.empty?
|
|
76
|
+
|
|
77
|
+
omitted = expected_files - files
|
|
78
|
+
raise "Package omitted spec.files entries: #{omitted.join(', ')}" unless omitted.empty?
|
|
79
|
+
|
|
80
|
+
PACKAGE_FORBIDDEN_PATTERNS.each do |label, pattern|
|
|
81
|
+
matches = files.grep(pattern)
|
|
82
|
+
raise "Package contains #{label}: #{matches.join(', ')}" unless matches.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
native_sources = files.grep(%r{\Aext/dry_validation_rust/src/.*\.rs\z})
|
|
86
|
+
raise 'Package is missing native Rust source files' if native_sources.empty?
|
|
87
|
+
|
|
88
|
+
files
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def with_unbundled_environment(&)
|
|
92
|
+
if defined?(Bundler)
|
|
93
|
+
Bundler.with_unbundled_env(&)
|
|
94
|
+
else
|
|
95
|
+
yield
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def rb_sys_gem_lib_path
|
|
100
|
+
File.join(Gem::Specification.find_by_name('rb_sys').full_gem_path, 'lib')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def predicate_manifest
|
|
104
|
+
manifest = YAML.safe_load_file(PREDICATE_MANIFEST_PATH, permitted_classes: [], aliases: false)
|
|
105
|
+
predicates = manifest.fetch('predicates')
|
|
106
|
+
raise 'predicates.yml predicates must be an array' unless predicates.is_a?(Array)
|
|
107
|
+
|
|
108
|
+
names = []
|
|
109
|
+
predicates.each do |predicate|
|
|
110
|
+
raise 'each predicate must be a mapping' unless predicate.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
name = predicate.fetch('name')
|
|
113
|
+
owner = predicate.fetch('owner')
|
|
114
|
+
ruby_method = predicate.fetch('ruby_method')
|
|
115
|
+
supported_types = predicate.fetch('supported_types')
|
|
116
|
+
raise "invalid predicate name: #{name.inspect}" unless name.is_a?(String) && /\A[a-z][a-z0-9_]*\z/.match?(name)
|
|
117
|
+
raise "duplicate predicate name: #{name}" if names.include?(name)
|
|
118
|
+
raise "invalid owner for #{name}: #{owner.inspect}" unless %w[rust ruby].include?(owner)
|
|
119
|
+
raise "ruby_method for #{name} must be a non-empty string" unless ruby_method.is_a?(String) && !ruby_method.empty?
|
|
120
|
+
unless supported_types.is_a?(Array) && !supported_types.empty?
|
|
121
|
+
raise "supported_types for #{name} must be a non-empty array"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if owner == 'rust'
|
|
125
|
+
rust_op = predicate.fetch('rust_op')
|
|
126
|
+
unless rust_op.is_a?(String) && /\A[A-Z][A-Za-z0-9]*\z/.match?(rust_op)
|
|
127
|
+
raise "invalid rust_op for #{name}: #{rust_op.inspect}"
|
|
128
|
+
end
|
|
129
|
+
elsif predicate.key?('rust_op')
|
|
130
|
+
raise "Ruby-owned predicate #{name} must not declare rust_op"
|
|
131
|
+
end
|
|
132
|
+
names << name
|
|
133
|
+
end
|
|
134
|
+
predicates
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def generated_predicate_files(predicates)
|
|
138
|
+
native = predicates.select { |predicate| predicate.fetch('owner') == 'rust' }
|
|
139
|
+
ruby = predicates.select { |predicate| predicate.fetch('owner') == 'ruby' }
|
|
140
|
+
ruby_symbols = ->(items) { items.map { |predicate| predicate.fetch('name') }.join(' ') }
|
|
141
|
+
|
|
142
|
+
ruby_source = <<~RUBY
|
|
143
|
+
# frozen_string_literal: true
|
|
144
|
+
|
|
145
|
+
# This file is generated by `bundle exec rake generate:predicates`.
|
|
146
|
+
# Do not edit it directly; update predicates.yml instead.
|
|
147
|
+
module Dry
|
|
148
|
+
module Validation
|
|
149
|
+
module Rust
|
|
150
|
+
class Schema
|
|
151
|
+
NATIVE_PREDICATES = %i[#{ruby_symbols.call(native)}].freeze
|
|
152
|
+
RUBY_PREDICATES = %i[#{ruby_symbols.call(ruby)}].freeze
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
RUBY
|
|
158
|
+
enum_variants = native.map { |predicate| " #{predicate.fetch('rust_op')}," }.join("\n")
|
|
159
|
+
name_mapping = native.map { |predicate| " \"#{predicate.fetch('name')}\" => Self::#{predicate.fetch('rust_op')}," }.join("\n")
|
|
160
|
+
rust_source = <<~RUST
|
|
161
|
+
// This file is generated by `bundle exec rake generate:predicates`.
|
|
162
|
+
// Do not edit it directly; update predicates.yml instead.
|
|
163
|
+
|
|
164
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
165
|
+
pub(crate) enum PredicateOp {
|
|
166
|
+
#{enum_variants}
|
|
167
|
+
Unsupported,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
impl PredicateOp {
|
|
171
|
+
fn from_name(name: &str) -> Self {
|
|
172
|
+
match name {
|
|
173
|
+
#{name_mapping}
|
|
174
|
+
_ => Self::Unsupported,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
RUST
|
|
179
|
+
{ GENERATED_RUBY_PREDICATES_PATH => ruby_source, GENERATED_RUST_PREDICATES_PATH => rust_source }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
namespace :generate do
|
|
183
|
+
desc 'Generate predicate ownership sources from predicates.yml'
|
|
184
|
+
task :predicates do
|
|
185
|
+
generated_predicate_files(predicate_manifest).each { |path, source| File.write(path, source) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
namespace :predicates do
|
|
189
|
+
desc 'Verify generated predicate ownership sources are in sync with predicates.yml'
|
|
190
|
+
task :check do
|
|
191
|
+
generated_predicate_files(predicate_manifest).each do |path, source|
|
|
192
|
+
unless File.exist?(path) && File.read(path) == source
|
|
193
|
+
raise "#{File.basename(path)} is out of sync; run bundle exec rake generate:predicates"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def smoke_installed_package(gem_path)
|
|
201
|
+
ruby_code = <<~'RUBY'
|
|
202
|
+
gem "dry-validation-rust"
|
|
203
|
+
require "dry/validation/rust"
|
|
204
|
+
|
|
205
|
+
loaded = Gem.loaded_specs.fetch("dry-validation-rust")
|
|
206
|
+
gem_home = ENV.fetch("GEM_HOME")
|
|
207
|
+
loaded_path = File.realpath(loaded.full_gem_path)
|
|
208
|
+
expected_path = File.realpath(gem_home)
|
|
209
|
+
loaded_from_gem_home = loaded_path == expected_path || loaded_path.start_with?("#{expected_path}#{File::SEPARATOR}")
|
|
210
|
+
abort "loaded gem from #{loaded_path}, expected #{expected_path}" unless loaded_from_gem_home
|
|
211
|
+
abort "loaded upstream dry-validation" if Gem.loaded_specs.key?("dry-validation")
|
|
212
|
+
|
|
213
|
+
contract = Class.new(Dry::Validation::Rust::Contract) do
|
|
214
|
+
params do
|
|
215
|
+
required(:age).value(:integer)
|
|
216
|
+
required(:name).filled(:string)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
rule(:age) do
|
|
220
|
+
key.failure("must be an adult") if value < 18
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
success = contract.new.call("age" => "21", "name" => "Jane")
|
|
225
|
+
abort success.errors.to_h.inspect unless success.success? && success.to_h == {age: 21, name: "Jane"}
|
|
226
|
+
|
|
227
|
+
failure = contract.new.call("age" => "17", "name" => "Jane")
|
|
228
|
+
abort failure.errors.to_h.inspect unless failure.failure? && failure.errors.to_h == {age: ["must be an adult"]}
|
|
229
|
+
RUBY
|
|
230
|
+
|
|
231
|
+
Dir.mktmpdir('dry-validation-rust-gem-home') do |gem_home|
|
|
232
|
+
Dir.mktmpdir('dry-validation-rust-package-smoke') do |workdir|
|
|
233
|
+
env = {
|
|
234
|
+
'GEM_HOME' => gem_home,
|
|
235
|
+
'GEM_PATH' => ([gem_home] + Gem.path).uniq.join(File::PATH_SEPARATOR),
|
|
236
|
+
'RB_SYS_GEM_LIB' => rb_sys_gem_lib_path
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
with_unbundled_environment do
|
|
240
|
+
sh env, 'gem', 'install', '--local', gem_path, '--no-document'
|
|
241
|
+
Dir.chdir(workdir) do
|
|
242
|
+
sh env, 'ruby', '-e', ruby_code
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
desc 'Compile the Rust extension'
|
|
250
|
+
task compile: 'generate:predicates' do
|
|
251
|
+
Dir.chdir(EXTENSION_DIR) do
|
|
252
|
+
ruby 'extconf.rb' if !File.exist?('Makefile') || File.mtime('extconf.rb') > File.mtime('Makefile')
|
|
253
|
+
sh 'make'
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
Rake::TestTask.new(test: :compile) do |task|
|
|
258
|
+
task.libs << 'lib' << 'test'
|
|
259
|
+
task.pattern = 'test/**/*_test.rb'
|
|
260
|
+
task.warning = true
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
spec = Gem::Specification.load(GEMSPEC_PATH)
|
|
264
|
+
|
|
265
|
+
spec.extensions.clear if ENV.key?('RUBY_TARGET')
|
|
266
|
+
|
|
267
|
+
Gem::PackageTask.new(spec)
|
|
268
|
+
|
|
269
|
+
Dir.chdir(EXTENSION_DIR) do
|
|
270
|
+
RbSys::ExtensionTask.new('dry_validation_rust_native', spec) do |ext|
|
|
271
|
+
# Cargo's package name locates the manifest; the shared-library name is
|
|
272
|
+
# `native`, matching `[lib] name` in Cargo.toml and the Ruby require path.
|
|
273
|
+
ext.name = 'native'
|
|
274
|
+
ext.lib_dir = 'lib/dry/validation/rust'
|
|
275
|
+
def ext.source_files
|
|
276
|
+
super.exclude("#{ext_dir}/fuzz/**/*", '**/fuzz/**/*')
|
|
277
|
+
end
|
|
278
|
+
unless ENV.key?('RUBY_TARGET')
|
|
279
|
+
ext.cross_compile = true
|
|
280
|
+
ext.cross_platform = CROSS_COMPILE_PLATFORMS
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
if ENV.key?('RUBY_TARGET')
|
|
286
|
+
# rb-sys-dock injects 'gem' task execution, but rake-compiler's ExtensionTask
|
|
287
|
+
# hooks the host 'native' compilation to the 'gem' task.
|
|
288
|
+
# This causes host compilation with a cross-compile target, breaking linking.
|
|
289
|
+
# We clear the host 'native' task from 'gem' to prevent this.
|
|
290
|
+
if Rake::Task.task_defined?('gem')
|
|
291
|
+
Rake::Task['gem'].prerequisites.delete('native')
|
|
292
|
+
Rake::Task['gem'].prerequisites.delete("pkg/#{spec.full_name}.gem")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Remove host extension file dependencies so they are never triggered
|
|
296
|
+
# during a cross-compile.
|
|
297
|
+
Rake.application.tasks.each do |t|
|
|
298
|
+
t.prerequisites.delete('lib/dry/validation/rust/native.so')
|
|
299
|
+
end
|
|
300
|
+
Rake::Task['lib/dry/validation/rust/native.so'].clear if Rake::Task.task_defined?('lib/dry/validation/rust/native.so')
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
file 'Cargo.lock' => File.join(EXTENSION_DIR, 'Cargo.lock')
|
|
304
|
+
file 'Cargo.toml' => File.join(EXTENSION_DIR, 'Cargo.toml')
|
|
305
|
+
|
|
306
|
+
namespace :package do
|
|
307
|
+
desc 'Build and audit the source gem package'
|
|
308
|
+
task :audit do
|
|
309
|
+
FileUtils.mkdir_p(File.expand_path('pkg', __dir__))
|
|
310
|
+
gem_path = File.expand_path("pkg/#{spec.full_name}.gem", __dir__)
|
|
311
|
+
|
|
312
|
+
sh 'gem', 'build', GEMSPEC_PATH, '--output', gem_path
|
|
313
|
+
validate_package_files(gem_path, spec.files.sort)
|
|
314
|
+
smoke_installed_package(gem_path)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
namespace :dependency do
|
|
319
|
+
desc 'Print dependency and tool versions for verification logs'
|
|
320
|
+
task :versions do
|
|
321
|
+
puts "Ruby: #{RUBY_DESCRIPTION}"
|
|
322
|
+
puts "RubyGems: #{Gem::VERSION}"
|
|
323
|
+
puts "Bundler: #{Bundler::VERSION}" if defined?(Bundler)
|
|
324
|
+
|
|
325
|
+
puts "\nBundled Ruby gems:"
|
|
326
|
+
Gem.loaded_specs.values
|
|
327
|
+
.select { |loaded_spec| loaded_spec.full_gem_path.start_with?(File.expand_path(__dir__)) || loaded_spec.name == 'dry-validation-rust' }
|
|
328
|
+
.sort_by(&:name)
|
|
329
|
+
.each { |loaded_spec| puts " #{loaded_spec.name} #{loaded_spec.version}" }
|
|
330
|
+
|
|
331
|
+
puts "\nLocked Ruby gems:"
|
|
332
|
+
Bundler.load.specs.sort_by(&:name).each { |locked_spec| puts " #{locked_spec.name} #{locked_spec.version}" }
|
|
333
|
+
|
|
334
|
+
puts "\nRust toolchain:"
|
|
335
|
+
sh 'rustc', '--version'
|
|
336
|
+
sh 'cargo', '--version'
|
|
337
|
+
|
|
338
|
+
puts "\nRust dependency tree:"
|
|
339
|
+
sh 'cargo', 'tree', '--locked', '--manifest-path', 'ext/dry_validation_rust/Cargo.toml', '--depth', '1'
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
namespace :compatibility do
|
|
344
|
+
desc 'Run the pinned upstream differential corpus in isolated Ruby processes'
|
|
345
|
+
task differential: :compile do
|
|
346
|
+
sh 'bundle', 'exec', 'ruby', '-Ilib', '-Itest', 'test/differential_compatibility_test.rb'
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
task default: :test
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported release lines
|
|
4
|
+
|
|
5
|
+
`dry-validation-rust` is currently an alpha project. Security fixes are
|
|
6
|
+
considered for the latest `0.1.x` prerelease and the current `main` branch on a
|
|
7
|
+
best-effort basis.
|
|
8
|
+
|
|
9
|
+
| Release line | Security support |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Latest `0.1.x` prerelease | Best effort |
|
|
12
|
+
| Older prereleases | Upgrade required |
|
|
13
|
+
| Unreleased development branches | No separate backport promise |
|
|
14
|
+
|
|
15
|
+
The project does not yet promise long-term support or security backports.
|
|
16
|
+
Platform and runtime targets are listed in
|
|
17
|
+
[docs/SUPPORT_MATRIX.md](docs/SUPPORT_MATRIX.md).
|
|
18
|
+
|
|
19
|
+
## Reporting a vulnerability
|
|
20
|
+
|
|
21
|
+
Do not open a public issue, discussion, or pull request for a suspected
|
|
22
|
+
vulnerability.
|
|
23
|
+
|
|
24
|
+
Use
|
|
25
|
+
[GitHub private vulnerability reporting](https://github.com/alex-tomilov/dry-validation-rust/security/advisories/new)
|
|
26
|
+
to send the report to the maintainer. If GitHub shows that private reporting is
|
|
27
|
+
unavailable, do not disclose the report publicly; contact the maintainer
|
|
28
|
+
through a private contact method listed on the maintainer's GitHub profile and
|
|
29
|
+
ask for a secure reporting channel.
|
|
30
|
+
|
|
31
|
+
Include:
|
|
32
|
+
|
|
33
|
+
- the affected gem version, commit, and loading mode;
|
|
34
|
+
- Ruby, Rust, OS, architecture, and source/native build details;
|
|
35
|
+
- a minimal reproducer or proof of concept;
|
|
36
|
+
- the expected and observed security boundary;
|
|
37
|
+
- impact, prerequisites, and known mitigations;
|
|
38
|
+
- whether the issue is already public or shared with anyone else;
|
|
39
|
+
- any preferred disclosure or credit details.
|
|
40
|
+
|
|
41
|
+
Do not include real credentials, private production data, or unnecessary
|
|
42
|
+
personal information.
|
|
43
|
+
|
|
44
|
+
## What to expect
|
|
45
|
+
|
|
46
|
+
The maintainer aims to acknowledge a complete report within seven calendar
|
|
47
|
+
days, but this is a target rather than an SLA. Triage may request additional
|
|
48
|
+
information or determine that the report is a correctness bug without a
|
|
49
|
+
security impact.
|
|
50
|
+
|
|
51
|
+
For an accepted vulnerability, the reporter and maintainer will coordinate on
|
|
52
|
+
impact, remediation, release timing, advisory text, and credit. Disclosure
|
|
53
|
+
should wait until a fix or practical mitigation is available, unless active
|
|
54
|
+
exploitation or another overriding public-interest concern requires a
|
|
55
|
+
different timeline.
|
|
56
|
+
|
|
57
|
+
## Coordinated disclosure and embargo
|
|
58
|
+
|
|
59
|
+
The project uses a 90-calendar-day embargo for public technical details after
|
|
60
|
+
a fixed version is released. During that period, the maintainer may publish a
|
|
61
|
+
minimal advisory and upgrade guidance, but will not publish a proof of concept
|
|
62
|
+
or detailed exploitation steps without coordinating with the reporter.
|
|
63
|
+
|
|
64
|
+
The embargo may end earlier only with the reporter's agreement, or when active
|
|
65
|
+
exploitation, an already-public disclosure, or another overriding
|
|
66
|
+
public-interest concern makes earlier disclosure necessary. At the end of the
|
|
67
|
+
embargo, the maintainer will publish the advisory through GitHub Security
|
|
68
|
+
Advisories when practical and credit the reporter if requested.
|
|
69
|
+
|
|
70
|
+
## Dependency-audit schedule
|
|
71
|
+
|
|
72
|
+
The [Security workflow](.github/workflows/security.yml) runs on pull requests,
|
|
73
|
+
pushes to `main` and `develop`, and every Monday. It runs `bundler-audit` for
|
|
74
|
+
Ruby dependencies and `cargo audit --deny warnings` for Rust dependencies.
|
|
75
|
+
Audit failures are handled under
|
|
76
|
+
[the dependency-security policy](docs/DEPENDENCY_SECURITY.md).
|
|
77
|
+
|
|
78
|
+
Security releases and advisories remain subject to maintainer approval. No
|
|
79
|
+
report grants permission to publish a gem, create a tag, or disclose private
|
|
80
|
+
project information.
|
|
81
|
+
|
|
82
|
+
## Gem signing and publication
|
|
83
|
+
|
|
84
|
+
Release gems are built only by the protected `rubygems:push` workflow. The
|
|
85
|
+
workflow signs every source and native gem with GitHub Actions OIDC and
|
|
86
|
+
Sigstore, then attaches the resulting `.sigstore.json` bundle alongside the
|
|
87
|
+
gem to the GitHub release.
|
|
88
|
+
|
|
89
|
+
RubyGems.org publication uses RubyGems Trusted Publishing through the same
|
|
90
|
+
OIDC identity; the repository does not keep a long-lived RubyGems API key for
|
|
91
|
+
this workflow. RubyGems Trusted Publishing must be configured on RubyGems.org
|
|
92
|
+
for `alex-tomilov/dry-validation-rust`, the `rubygems:push` workflow, and the
|
|
93
|
+
GitHub `release` environment before a release can publish.
|
|
94
|
+
|
|
95
|
+
The `release` environment is an approval boundary. Maintainers must review the
|
|
96
|
+
tag and generated artifacts before approving it. A test publication, when
|
|
97
|
+
needed, must use a separate RubyGems test-host trusted-publisher configuration
|
|
98
|
+
and must not change the production publisher or release environment.
|
data/SECURITY_AUDIT.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Rust supply-chain audit guide
|
|
2
|
+
|
|
3
|
+
`cargo vet --locked` is a required check in the security workflow. Its
|
|
4
|
+
repository-managed records are in `supply-chain/`; `imports.lock` pins the
|
|
5
|
+
Mozilla audit set so CI does not fetch or silently change its trust inputs.
|
|
6
|
+
|
|
7
|
+
## Attack surface
|
|
8
|
+
|
|
9
|
+
The native extension processes untrusted validation plans and values. Review
|
|
10
|
+
changes around these areas especially carefully:
|
|
11
|
+
|
|
12
|
+
- `serde_json` parsing, including deeply nested or unexpectedly shaped JSON;
|
|
13
|
+
- bindgen and its build-time `libclang` dependency;
|
|
14
|
+
- the Magnus and rb-sys Ruby bridge, including all `unsafe` code and Ruby
|
|
15
|
+
object lifetime or exception boundaries.
|
|
16
|
+
|
|
17
|
+
## Dependency audit policy
|
|
18
|
+
|
|
19
|
+
Lockfile and `supply-chain/` changes are reviewed together. A new third-party
|
|
20
|
+
crate must have a `safe-to-run` or `safe-to-deploy` audit path from a reviewed
|
|
21
|
+
local audit or a pinned trusted import before it can merge. Do not add an
|
|
22
|
+
exemption for a new dependency as a substitute for that review.
|
|
23
|
+
|
|
24
|
+
Existing exemptions are the audited baseline recorded when cargo-vet was
|
|
25
|
+
introduced. Reduce them when a suitable trusted audit becomes available.
|
|
26
|
+
|
|
27
|
+
## CVE response
|
|
28
|
+
|
|
29
|
+
Treat a new advisory as a security issue: identify affected supported builds,
|
|
30
|
+
assess exploitability at the attack surfaces above, then update, remove, or
|
|
31
|
+
replace the dependency. If immediate remediation is impossible, record a
|
|
32
|
+
dated exception with mitigation and an owner in
|
|
33
|
+
`docs/DEPENDENCY_SECURITY.md`; remove it once resolved.
|
data/SUMMARY.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Summary
|
|
2
|
+
|
|
3
|
+
- [Overview](README.md)
|
|
4
|
+
- [Roadmap](docs/ROADMAP.md)
|
|
5
|
+
|
|
6
|
+
## Using dry-validation-rust
|
|
7
|
+
|
|
8
|
+
- [Compatibility](docs/COMPATIBILITY.md)
|
|
9
|
+
- [Support matrix](docs/SUPPORT_MATRIX.md)
|
|
10
|
+
- [API stability](docs/API_STABILITY.md)
|
|
11
|
+
- [Migration guide](docs/migration.md)
|
|
12
|
+
- [Migration recipes](docs/MIGRATION_RECIPES.md)
|
|
13
|
+
- [Windows](docs/WINDOWS.md)
|
|
14
|
+
|
|
15
|
+
## Reference
|
|
16
|
+
|
|
17
|
+
- [Ruby API reference](docs/ruby-api-reference.md)
|
|
18
|
+
- [Architecture](docs/ARCHITECTURE.md)
|
|
19
|
+
- [API boundary](docs/API_BOUNDARY.md)
|
|
20
|
+
- [Benchmarking](docs/BENCHMARKING.md)
|
|
21
|
+
- [Memory benchmarking](docs/MEMORY_BENCHMARKING.md)
|
|
22
|
+
- [Profiling](docs/PROFILING.md)
|
|
23
|
+
- [Verification](docs/VERIFICATION.md)
|
|
24
|
+
- [Dependency security](docs/DEPENDENCY_SECURITY.md)
|
|
25
|
+
- [Feasibility](docs/FEASIBILITY.md)
|
|
26
|
+
|
|
27
|
+
## Project
|
|
28
|
+
|
|
29
|
+
- [Project management](docs/PROJECT_MANAGEMENT.md)
|
|
30
|
+
- [Release checklist](docs/RELEASE_CHECKLIST.md)
|
data/SUPPORT.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Support
|
|
2
|
+
|
|
3
|
+
`dry-validation-rust` is an alpha, single-maintainer project. Support is
|
|
4
|
+
best-effort and no response-time or resolution SLA is offered.
|
|
5
|
+
|
|
6
|
+
Before opening an issue, check the
|
|
7
|
+
[support matrix](docs/SUPPORT_MATRIX.md),
|
|
8
|
+
[compatibility matrix](docs/COMPATIBILITY.md), and existing issues.
|
|
9
|
+
|
|
10
|
+
## Bugs
|
|
11
|
+
|
|
12
|
+
Use the bug report form for behavior that appears incorrect within the
|
|
13
|
+
documented supported surface. Include a minimal contract and input, exact
|
|
14
|
+
versions, loading mode, build type, platform, expected and actual output, and a
|
|
15
|
+
full backtrace where applicable.
|
|
16
|
+
|
|
17
|
+
Native crashes, installation failures, and unexpected Ruby exceptions are
|
|
18
|
+
bugs even when they occur around unsupported input. Unsupported DSL behavior
|
|
19
|
+
should fail loudly rather than silently succeed.
|
|
20
|
+
|
|
21
|
+
## Compatibility mismatches
|
|
22
|
+
|
|
23
|
+
Use the compatibility mismatch form when the Rust implementation and pinned
|
|
24
|
+
upstream `dry-validation`/`dry-schema` releases produce different values,
|
|
25
|
+
errors, metadata, exceptions, or loading behavior for a documented compatible
|
|
26
|
+
feature. Run the two implementations in separate processes and include both
|
|
27
|
+
outputs and exact upstream versions.
|
|
28
|
+
|
|
29
|
+
Not every upstream feature is in scope. Check `docs/COMPATIBILITY.md` before
|
|
30
|
+
filing.
|
|
31
|
+
|
|
32
|
+
## Feature requests
|
|
33
|
+
|
|
34
|
+
Use the feature request form to describe the user problem, desired behavior,
|
|
35
|
+
alternatives, and compatibility implications. New DSL surface requires
|
|
36
|
+
explicit design and tests; it will not be added as a side effect of another
|
|
37
|
+
change.
|
|
38
|
+
|
|
39
|
+
## Usage questions
|
|
40
|
+
|
|
41
|
+
Use GitHub Discussions when that repository feature is available. If it is not
|
|
42
|
+
available, consult the README, examples, compatibility matrix, and existing
|
|
43
|
+
issues. Open an issue only when the question identifies a likely documentation
|
|
44
|
+
gap, bug, or focused feature request.
|
|
45
|
+
|
|
46
|
+
General application debugging, contract design consulting, and upstream
|
|
47
|
+
`dry-validation` support are outside this project's support scope.
|
|
48
|
+
|
|
49
|
+
## Performance reports
|
|
50
|
+
|
|
51
|
+
Use the performance report form. Reports must include a runnable reproducer,
|
|
52
|
+
warmup, iterations, hardware/software details, allocations or RSS where
|
|
53
|
+
relevant, and raw results from multiple runs. A single benchmark number without
|
|
54
|
+
semantic equivalence evidence is not actionable.
|
|
55
|
+
|
|
56
|
+
## Security reports
|
|
57
|
+
|
|
58
|
+
Do not use public issues or discussions for suspected vulnerabilities. Follow
|
|
59
|
+
[SECURITY.md](SECURITY.md) and use GitHub private vulnerability reporting.
|
|
60
|
+
|
|
61
|
+
## Triage expectations
|
|
62
|
+
|
|
63
|
+
The maintainer targets initial triage of complete bug and compatibility reports
|
|
64
|
+
within two weeks when capacity permits. Feature requests, performance
|
|
65
|
+
investigations, and usage questions may take longer or receive no immediate
|
|
66
|
+
response. Incomplete or out-of-scope reports may be closed with a request for
|
|
67
|
+
the missing information.
|
data/book.toml
ADDED