Module: Ibex::GenerationTransactionValidation
- Included in:
- GenerationTransaction
- Defined in:
- lib/ibex/generation_transaction_validation.rb,
sig/ibex/generation_transaction_validation.rbs
Overview
Canonical target, source, symlink, and lock identity checks for generation.
Instance Method Summary collapse
- #ensure_sources_stable!(phase) ⇒ void
- #existing_target(expanded, validate_links) ⇒ String
- #paths_alias?(left, right) ⇒ Boolean
-
#portable_target_key(path) ⇒ [ String, String ]
Reject target names that collide on common case-insensitive or normalization-insensitive filesystems, even when the current filesystem would permit both names.
- #protected_generation_paths ⇒ Array[String]
- #protected_generation_stats ⇒ Array[File::Stat]
-
#resolved_target(path, seen = {}, validate_links: true) ⇒ String
Follow a final symlink while preserving the symlink path itself during publication.
- #same_inode?(left, right) ⇒ Boolean
- #stable_lock_identity?(path_stat, lock_stat) ⇒ Boolean
- #validate_active_locks!(phase) ⇒ void
- #validate_current_target!(record, sources, phase) ⇒ void
- #validate_current_targets!(phase) ⇒ void
- #validate_existing_target!(target) ⇒ void
- #validate_lock_boundaries!(lock_paths) ⇒ void
- #validate_lock_identity!(path, lock) ⇒ void
- #validate_locked_targets! ⇒ void
- #validate_source_boundaries! ⇒ void
Instance Method Details
#ensure_sources_stable!(phase) ⇒ void
This method returns an undefined value.
141 142 143 144 145 146 147 |
# File 'lib/ibex/generation_transaction_validation.rb', line 141 def ensure_sources_stable!(phase) validate_active_locks!(phase) validate_current_targets!(phase) return if @stability_check.call raise GenerationTransaction::SourceChanged, "(generation):1:1: source changed #{phase}" end |
#existing_target(expanded, validate_links) ⇒ String
32 33 34 35 36 |
# File 'lib/ibex/generation_transaction_validation.rb', line 32 def existing_target(, validate_links) target = File.realpath() validate_existing_target!(target) if validate_links target end |
#paths_alias?(left, right) ⇒ Boolean
116 117 118 119 120 121 122 123 124 |
# File 'lib/ibex/generation_transaction_validation.rb', line 116 def paths_alias?(left, right) return true if File.(left) == File.(right) return true if portable_target_key(left) == portable_target_key(right) return false unless File.exist?(left) && File.exist?(right) File.identical?(left, right) rescue SystemCallError true end |
#portable_target_key(path) ⇒ [ String, String ]
Reject target names that collide on common case-insensitive or normalization-insensitive filesystems, even when the current filesystem would permit both names.
130 131 132 133 134 135 136 137 138 |
# File 'lib/ibex/generation_transaction_validation.rb', line 130 def portable_target_key(path) basename = File.basename(path) folded = if basename.encoding == Encoding::UTF_8 && basename.valid_encoding? basename.unicode_normalize(:nfc).downcase(:fold).unicode_normalize(:nfc) else basename.downcase end [File.dirname(path), folded] end |
#protected_generation_paths ⇒ Array[String]
96 97 98 |
# File 'lib/ibex/generation_transaction_validation.rb', line 96 def protected_generation_paths @source_records.map(&:path) + @records.map { |record| record.fetch(:target) } end |
#protected_generation_stats ⇒ Array[File::Stat]
101 102 103 |
# File 'lib/ibex/generation_transaction_validation.rb', line 101 def protected_generation_stats protected_generation_paths.filter_map { |path| File.stat(path) if File.exist?(path) } end |
#resolved_target(path, seen = {}, validate_links: true) ⇒ String
Follow a final symlink while preserving the symlink path itself during publication.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/ibex/generation_transaction_validation.rb', line 14 def resolved_target(path, seen = {}, validate_links: true) = File.(path) if File.symlink?() raise Errno::ELOOP, if seen[] seen[] = true linked = File.(File.readlink(), File.dirname()) return resolved_target(linked, seen, validate_links: validate_links) end return existing_target(, validate_links) if File.exist?() parent = File.realpath(File.dirname()) raise Errno::ENOTDIR, parent unless File.directory?(parent) File.join(parent, File.basename()) end |
#same_inode?(left, right) ⇒ Boolean
111 112 113 |
# File 'lib/ibex/generation_transaction_validation.rb', line 111 def same_inode?(left, right) left.dev == right.dev && left.ino == right.ino end |
#stable_lock_identity?(path_stat, lock_stat) ⇒ Boolean
106 107 108 |
# File 'lib/ibex/generation_transaction_validation.rb', line 106 def stable_lock_identity?(path_stat, lock_stat) path_stat.file? && path_stat.nlink == 1 && same_inode?(path_stat, lock_stat) end |
#validate_active_locks!(phase) ⇒ void
This method returns an undefined value.
150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/ibex/generation_transaction_validation.rb', line 150 def validate_active_locks!(phase) @locked_paths.zip(@locks).each do |path, lock| next if stable_lock_identity?(File.lstat(path), lock.stat) raise GenerationTransaction::SourceChanged, "(generation):1:1: generation lock changed #{phase}: #{path}" end rescue SystemCallError => e raise GenerationTransaction::SourceChanged, "(generation):1:1: cannot revalidate generation lock #{phase}: #{e.}" end |
#validate_current_target!(record, sources, phase) ⇒ void
This method returns an undefined value.
174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/ibex/generation_transaction_validation.rb', line 174 def validate_current_target!(record, sources, phase) artifact = record.fetch(:artifact) current = resolved_target(artifact.path, validate_links: false) unless current == record.fetch(:target) raise GenerationTransaction::SourceChanged, "(generation):1:1: artifact target changed #{phase}: #{artifact.path}" end return unless sources.any? { |source| paths_alias?(current, source) } raise GenerationTransaction::SourceChanged, "(generation):1:1: artifact target aliases an input #{phase}: #{artifact.path}" end |
#validate_current_targets!(phase) ⇒ void
This method returns an undefined value.
163 164 165 166 167 168 169 170 171 |
# File 'lib/ibex/generation_transaction_validation.rb', line 163 def validate_current_targets!(phase) sources = @source_records.map(&:path) @records.each do |record| validate_current_target!(record, sources, phase) end rescue SystemCallError => e raise GenerationTransaction::SourceChanged, "(generation):1:1: cannot revalidate artifact target #{phase}: #{e.}" end |
#validate_existing_target!(target) ⇒ void
This method returns an undefined value.
39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/ibex/generation_transaction_validation.rb', line 39 def validate_existing_target!(target) stat = File.stat(target) unless stat.file? raise GenerationTransaction::Error, "(generation):1:1: output target is not a regular file: #{target}" end return unless stat.nlink > 1 raise GenerationTransaction::Error, "(generation):1:1: output target has multiple hard links: #{target}" end |
#validate_lock_boundaries!(lock_paths) ⇒ void
This method returns an undefined value.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/ibex/generation_transaction_validation.rb', line 65 def validate_lock_boundaries!(lock_paths) protected_paths = protected_generation_paths lock_paths.each do |path| if File.symlink?(path) raise GenerationTransaction::Error, "(generation):1:1: generation lock must not be a symlink: #{path}" end collision = protected_paths.find { |protected| paths_alias?(path, protected) } next unless collision raise GenerationTransaction::Error, "(generation):1:1: generation lock aliases a protected path: #{path} (#{collision})" end end |
#validate_lock_identity!(path, lock) ⇒ void
This method returns an undefined value.
82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/ibex/generation_transaction_validation.rb', line 82 def validate_lock_identity!(path, lock) path_stat = File.lstat(path) lock_stat = lock.stat unless stable_lock_identity?(path_stat, lock_stat) raise GenerationTransaction::Error, "(generation):1:1: generation lock identity changed while opening: #{path}" end return unless protected_generation_stats.any? { |stat| same_inode?(stat, lock_stat) } raise GenerationTransaction::Error, "(generation):1:1: generation lock aliases a protected inode: #{path}" end |
#validate_locked_targets! ⇒ void
This method returns an undefined value.
188 189 190 191 192 193 194 |
# File 'lib/ibex/generation_transaction_validation.rb', line 188 def validate_locked_targets! current = @records.map { |record| lock_path(record.fetch(:target)) }.uniq.sort return if current == @locked_paths raise GenerationTransaction::Error, "(generation):1:1: output target changed while acquiring generation locks" end |
#validate_source_boundaries! ⇒ void
This method returns an undefined value.
52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/ibex/generation_transaction_validation.rb', line 52 def validate_source_boundaries! source_paths = @source_records.map(&:path) @records.each do |record| target = record.fetch(:target) collision = source_paths.find { |source| paths_alias?(target, source) } next unless collision raise GenerationTransaction::Error, "(generation):1:1: artifact target aliases generation input: #{target} (#{collision})" end end |