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

Instance Method Details

#ensure_sources_stable!(phase) ⇒ void

This method returns an undefined value.

RBS:

  • (String phase) -> void

Parameters:

  • phase (String)


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

RBS:

  • (String expanded, bool validate_links) -> String

Parameters:

  • expanded (String)
  • validate_links (Boolean)

Returns:

  • (String)


32
33
34
35
36
# File 'lib/ibex/generation_transaction_validation.rb', line 32

def existing_target(expanded, validate_links)
  target = File.realpath(expanded)
  validate_existing_target!(target) if validate_links
  target
end

#paths_alias?(left, right) ⇒ Boolean

RBS:

  • (String left, String right) -> bool

Parameters:

  • left (String)
  • right (String)

Returns:

  • (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.expand_path(left) == File.expand_path(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.

RBS:

  • (String path) -> [String, String]

Parameters:

  • path (String)

Returns:

  • ([ String, String ])


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_pathsArray[String]

RBS:

  • () -> Array[String]

Returns:

  • (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_statsArray[File::Stat]

RBS:

  • () -> Array[File::Stat]

Returns:

  • (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.

RBS:

  • (String path, ?Hash[String, bool] seen, ?validate_links: bool) -> String

Parameters:

  • path (String)
  • seen (Hash[String, bool]) (defaults to: {})
  • validate_links: (Boolean) (defaults to: true)

Returns:

  • (String)


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)
  expanded = File.expand_path(path)
  if File.symlink?(expanded)
    raise Errno::ELOOP, expanded if seen[expanded]

    seen[expanded] = true
    linked = File.expand_path(File.readlink(expanded), File.dirname(expanded))
    return resolved_target(linked, seen, validate_links: validate_links)
  end
  return existing_target(expanded, validate_links) if File.exist?(expanded)

  parent = File.realpath(File.dirname(expanded))
  raise Errno::ENOTDIR, parent unless File.directory?(parent)

  File.join(parent, File.basename(expanded))
end

#same_inode?(left, right) ⇒ Boolean

RBS:

  • (File::Stat left, File::Stat right) -> bool

Parameters:

  • left (File::Stat)
  • right (File::Stat)

Returns:

  • (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

RBS:

  • (File::Stat path_stat, File::Stat lock_stat) -> bool

Parameters:

  • path_stat (File::Stat)
  • lock_stat (File::Stat)

Returns:

  • (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.

RBS:

  • (String phase) -> void

Parameters:

  • phase (String)


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.message}"
end

#validate_current_target!(record, sources, phase) ⇒ void

This method returns an undefined value.

RBS:

  • (Hash[Symbol, untyped] record, Array[String] sources, String phase) -> void

Parameters:

  • record (Hash[Symbol, untyped])
  • sources (Array[String])
  • phase (String)


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.

RBS:

  • (String phase) -> void

Parameters:

  • phase (String)


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.message}"
end

#validate_existing_target!(target) ⇒ void

This method returns an undefined value.

RBS:

  • (String target) -> void

Parameters:

  • target (String)


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.

RBS:

  • (Array[String] lock_paths) -> void

Parameters:

  • lock_paths (Array[String])


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.

RBS:

  • (String path, File lock) -> void

Parameters:

  • path (String)
  • lock (File)


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.

RBS:

  • () -> void



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.

RBS:

  • () -> void



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