Module: Metaclean::FileOps
- Defined in:
- lib/metaclean/file_ops.rb
Class Method Summary collapse
- .component_identities(path) ⇒ Object
- .copy_exclusive(src, dest, preserve: false, expected: nil) ⇒ Object
- .ensure_path_guard!(path, expected) ⇒ Object
- .ensure_same_file!(path, expected) ⇒ Object
- .find_executable(name) ⇒ Object
- .lexist?(path) ⇒ Boolean
- .metadata_copy_supported? ⇒ Boolean
- .native_metadata_copy!(source, destination) ⇒ Object
- .path_components(path) ⇒ Object
- .path_guard!(path) ⇒ Object
- .prepare_in_place_commit!(cleaned, source, expected) ⇒ Object
- .regular_stat!(path) ⇒ Object
- .restore_metadata(path, source_stat, strict: false) ⇒ Object
- .same_file?(left, right) ⇒ Boolean
- .same_identity?(left, right) ⇒ Boolean
- .secure_workspace!(directory) ⇒ Object
- .trusted_system_symlink?(path, stat) ⇒ Boolean
- .validate_output!(path) ⇒ Object
- .with_private_workspace(path, label) ⇒ Object
Class Method Details
.component_identities(path) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/metaclean/file_ops.rb', line 27 def component_identities(path) components = path_components(path) components.map.with_index do |component, index| stat = File.lstat(component) if stat.symlink? && (index == components.length - 1 || !trusted_system_symlink?(component, stat)) raise Error, "#{path} contains a symlink component (#{component}) — refusing to use it" end [component, stat.dev, stat.ino] end end |
.copy_exclusive(src, dest, preserve: false, expected: nil) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/metaclean/file_ops.rb', line 81 def copy_exclusive(src, dest, preserve: false, expected: nil) source = regular_stat!(src) if expected && !same_file?(source, expected) raise Error, "#{src} changed before it could be copied" end mode = source.mode & 0o7777 created = false read_flags = File::RDONLY read_flags |= File::NOFOLLOW if defined?(File::NOFOLLOW) File.open(dest, File::WRONLY | File::CREAT | File::EXCL, mode) do |out| created = true File.open(src, read_flags) do |input| opened = input.stat raise Error, "#{src} changed while opening — refusing to copy it" unless same_file?(opened, source) size = opened.size mtime = opened.mtime ctime = opened.ctime IO.copy_stream(input, out) finished = input.stat unless finished.size == size && finished.mtime == mtime && finished.ctime == ctime raise Error, "#{src} changed while it was being copied" end end end (dest, source) if preserve source rescue StandardError, Interrupt File.delete(dest) if created && dest && File.exist?(dest) raise end |
.ensure_path_guard!(path, expected) ⇒ Object
20 21 22 23 24 25 |
# File 'lib/metaclean/file_ops.rb', line 20 def ensure_path_guard!(path, expected) current = path_guard!(path) return if current == expected raise Error, "#{path} or one of its parent directories changed during cleaning" end |
.ensure_same_file!(path, expected) ⇒ Object
64 65 66 67 68 69 |
# File 'lib/metaclean/file_ops.rb', line 64 def ensure_same_file!(path, expected) current = regular_stat!(path) return current if same_file?(current, expected) raise Error, "#{path} changed during cleaning — refusing to commit" end |
.find_executable(name) ⇒ Object
169 170 171 172 173 174 175 176 177 |
# File 'lib/metaclean/file_ops.rb', line 169 def find_executable(name) ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |directory| next if directory.empty? candidate = File.join(directory, name) return candidate if File.file?(candidate) && File.executable?(candidate) end nil end |
.lexist?(path) ⇒ Boolean
184 185 186 |
# File 'lib/metaclean/file_ops.rb', line 184 def lexist?(path) File.symlink?(path) || File.exist?(path) end |
.metadata_copy_supported? ⇒ Boolean
179 180 181 182 |
# File 'lib/metaclean/file_ops.rb', line 179 def (RUBY_PLATFORM.include?('darwin') || RUBY_PLATFORM.include?('linux')) && !find_executable('cp').nil? end |
.native_metadata_copy!(source, destination) ⇒ Object
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/metaclean/file_ops.rb', line 152 def (source, destination) cp = find_executable('cp') raise Error, 'cp is required to preserve in-place filesystem metadata' unless cp args = if RUBY_PLATFORM.include?('darwin') [cp, '-p', '--', source, destination] elsif RUBY_PLATFORM.include?('linux') [cp, '--preserve=all', '--', source, destination] else raise Error, "In-place metadata preservation is unsupported on #{RUBY_PLATFORM}" end _out, err, status = Metaclean.capture3(*args) return if status.success? raise Error, "Could not preserve filesystem metadata for in-place output: #{err.strip}" end |
.path_components(path) ⇒ Object
39 40 41 42 43 44 45 |
# File 'lib/metaclean/file_ops.rb', line 39 def path_components(path) = File.(path) current = File::SEPARATOR .split(File::SEPARATOR).reject(&:empty?).map do |part| current = File.join(current, part) end end |
.path_guard!(path) ⇒ Object
10 11 12 13 14 15 16 17 18 |
# File 'lib/metaclean/file_ops.rb', line 10 def path_guard!(path) components = component_identities(path) realpath = File.realpath(path) raise Error, "#{path} changed while its path was being verified" unless components == component_identities(path) { realpath: realpath, components: components } rescue SystemCallError => e raise Error, "#{path} cannot be resolved safely: #{e.}" end |
.prepare_in_place_commit!(cleaned, source, expected) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/metaclean/file_ops.rb', line 125 def prepare_in_place_commit!(cleaned, source, expected) ensure_same_file!(source, expected) prepared = "#{cleaned}.metadata" (source, prepared) copied = regular_stat!(prepared) unless copied.uid == expected.uid && copied.gid == expected.gid raise Error, 'Could not preserve source ownership for in-place output' end clean_stat = regular_stat!(cleaned) File.chmod((expected.mode & 0o7777) | 0o200, prepared) write_flags = File::WRONLY | File::TRUNC write_flags |= File::NOFOLLOW if defined?(File::NOFOLLOW) File.open(prepared, write_flags) do |out| File.open(cleaned, File::RDONLY) { |input| IO.copy_stream(input, out) } end ensure_same_file!(cleaned, clean_stat) ensure_same_file!(source, expected) (prepared, expected, strict: true) validate_output!(prepared) File.rename(prepared, cleaned) cleaned rescue StandardError, Interrupt File.delete(prepared) if prepared && lexist?(prepared) raise end |
.regular_stat!(path) ⇒ Object
54 55 56 57 58 59 60 61 62 |
# File 'lib/metaclean/file_ops.rb', line 54 def regular_stat!(path) stat = File.lstat(path) raise Error, "#{path} is a symlink — refusing to use it" if stat.symlink? raise Error, "#{path} is not a regular file — refusing to use it" unless stat.file? stat rescue SystemCallError => e raise Error, "#{path} cannot be used: #{e.}" end |
.restore_metadata(path, source_stat, strict: false) ⇒ Object
116 117 118 119 120 121 122 123 |
# File 'lib/metaclean/file_ops.rb', line 116 def (path, source_stat, strict: false) File.chmod(source_stat.mode & 0o7777, path) File.utime(source_stat.atime, source_stat.mtime, path) rescue SystemCallError => e raise Error, "Could not restore file permissions/timestamps: #{e.}" if strict nil end |
.same_file?(left, right) ⇒ Boolean
71 72 73 74 |
# File 'lib/metaclean/file_ops.rb', line 71 def same_file?(left, right) left.dev == right.dev && left.ino == right.ino && left.size == right.size && left.mtime == right.mtime && left.ctime == right.ctime end |
.same_identity?(left, right) ⇒ Boolean
76 77 78 79 |
# File 'lib/metaclean/file_ops.rb', line 76 def same_identity?(left, right) left.dev == right.dev && left.ino == right.ino && left.size == right.size && left.mtime == right.mtime end |
.secure_workspace!(directory) ⇒ Object
204 205 206 207 208 209 210 211 212 |
# File 'lib/metaclean/file_ops.rb', line 204 def secure_workspace!(directory) File.chmod(0o700, directory) mode = File.stat(directory).mode & 0o777 return true if mode.nobits?(0o077) raise Error, "#{directory} cannot enforce a private workspace (mode #{mode.to_s(8)})" rescue SystemCallError => e raise Error, "#{directory} cannot enforce a private workspace: #{e.}" end |
.trusted_system_symlink?(path, stat) ⇒ Boolean
47 48 49 50 51 52 |
# File 'lib/metaclean/file_ops.rb', line 47 def trusted_system_symlink?(path, stat) parent = File.lstat(File.dirname(path)) stat.uid.zero? && parent.uid.zero? && parent.mode.nobits?(0o022) rescue SystemCallError false end |
.validate_output!(path) ⇒ Object
188 189 190 191 192 193 |
# File 'lib/metaclean/file_ops.rb', line 188 def validate_output!(path) stat = regular_stat!(path) raise Error, "#{path} is empty — refusing to use tool output" unless stat.size.positive? stat end |
.with_private_workspace(path, label) ⇒ Object
195 196 197 198 199 200 201 202 |
# File 'lib/metaclean/file_ops.rb', line 195 def with_private_workspace(path, label) parent = File.dirname(File.(path)) prefix = "#{TMP_MARKER}#{label}.#{Process.pid}." Dir.mktmpdir(prefix, parent) do |dir| secure_workspace!(dir) yield dir end end |