Skip to content

Instantly share code, notes, and snippets.

@garybernhardt
Created March 14, 2017 22:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save garybernhardt/a5e166653605c43b048cffbf5333edf3 to your computer and use it in GitHub Desktop.
Save garybernhardt/a5e166653605c43b048cffbf5333edf3 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# This script tests par2 recovery when the par2 files themselves are corrupted.
# Process:
# 1. Generate a file containing all 256 possible bytes.
# (More would be better, but it gets slow fast.)
# 2. Generate par2 data for the file.
# 3. Individually corrupt each par2 file at each offset.
# (Write byte 0 unless the offset already contains byte 0; then, write byte 255.)
# (Writing each possible byte would be better, but it gets slow fast.)
# 4. After each corruption, verify the par2 data, then reverse the corruption.
# 5. Produce a summary of what the par2 verification commands' output, along with the frequencies of each output string.
#
# par2 0.6.14 passes this test.
require "fileutils"
TEMP_DIR = "temp_dir"
RESULTS = Hash.new { 0 }
def in_temp_dir(&block)
if Dir.exists?(TEMP_DIR)
FileUtils.rm_rf(TEMP_DIR)
end
Dir.mkdir(TEMP_DIR)
Dir.chdir(TEMP_DIR, &block)
end
def create_par_archive
# Write a file containing all 256 bytes.
test_data = (0...256).map(&:chr).join("")
File.write("data_file", test_data)
# Create the par archive.
`par2 create -R data.par2 data_file`
# Corrupt the data file (the par archive is now the only source of correct data.)
File.write("data_file", "x", 0)
end
def corrupt_file_at_all_offsets(path)
size = File.stat(path).size
# Corrupt each byte of the file separately.
(0...size).each do |offset|
# Back up existing character.
old_char = File.read(path, 1, offset)
# Generate a new character to corrupt with.
corruption_char = old_char == 0.chr ? 255.chr : 0.chr
# Corrupt file.
File.write(path, corruption_char, offset)
# Verify. Par2 exits with 0 on success, 1 if it can recover, and 2 if it
# couldn't recover. Because we're verifying a corrupted file, we always
# expect exit status 1.
result = `par2 verify data.par2`
unless $?.exitstatus == 1
puts
puts result
puts
raise RuntimeError.new("failed")
end
# Tally the par2 verification output text by the number of times it
# occurred.
RESULTS[result] += 1
# Restore.
File.write(path, old_char, offset)
# Print progress.
puts "#{path} %2i%%" % (100 * offset.to_f / size)
end
end
def summarize
RESULTS.to_a.sort_by { |message, times| times }.each do |message, times|
puts
puts "==== This message occurred #{times} times ".ljust(79, "=")
puts
puts message
end
end
def main
in_temp_dir do
create_par_archive
Dir["*.par2"].each do |path|
corrupt_file_at_all_offsets(path)
end
summarize
end
end
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment