#!/bin/env ruby
|
# PAR2 File Parser for Ruby
|
# Bare essentials (but with room to grow) to get a listing of all files
|
#
|
# Note: This was written on Cygwin, there may be things that need to be tweaked.
|
# [email protected]
|
#
|
# Ref: http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
|
require 'FileUtils'
|
|
# Example - Par2FileListing will return Par2FileDescriptionPackets only
|
# This would normally be our main() equivalent
|
class Par2FileListing
|
def initialize(file)
|
@pkFile = Par2PacketFile.new(file)
|
end # init
|
def each
|
debug = false
|
@pkFile.each{|thepkt|
|
# Option 1: Iterate over each packet type calling Test():
|
# print "FileDesc!\n" if Par2FileDescriptionPacket.Test(thepkt)
|
# Option 2: Use a case statement checking Sig:
|
case thepkt['hdr'].type
|
when Par2FileDescriptionPacket.Sig
|
descPkt = Par2FileDescriptionPacket.new(thepkt)
|
print "#{descPkt.fileName} (#{descPkt.fileLength} bytes)\n" if debug
|
yield descPkt
|
# At this time, I don't care about any others...
|
else
|
print "Unknown packet type: '#{thepkt['hdr'].type}'\n" if debug
|
end # case
|
} # pkFile
|
end # next_file
|
end # Par2FileListing
|
|
# The header of every packet type
|
class Par2PacketHeader
|
attr_accessor :magic, :length, :hash, :setID, :type
|
def initialize(raw_header)
|
@magic = raw_header[0..7]
|
throw RuntimeError.new("Header check failed") if @magic.slice(/PAR2\0PKT/).nil?
|
@length = raw_header[8..15].unpack('Q')[0]
|
@hash = raw_header[16..31] # TODO: Need to convert to something useful
|
@setID = raw_header[32..47] # TODO: Need to convert to something useful
|
@type = raw_header[48..63]
|
end # init
|
end # Par2PacketHeader
|
|
# The file I/O class
|
class Par2PacketFile
|
# attr_accessor :data
|
def initialize(infile)
|
begin
|
@data = Array.new
|
fh = File.open(infile, "r");
|
while true
|
# Read in a PAR2 header
|
header = Par2PacketHeader.new(fh.sysread(64));
|
@data << {'hdr' => header, 'data' => fh.sysread(header.length-64)}
|
end # while
|
rescue EOFError
|
fh.close
|
end # rescue
|
end
|
def each
|
@data.each{|x| yield x}
|
end
|
end # Par2PacketFile
|
|
# A base interface for all packet types
|
class Par2BasePacket
|
attr_accessor :setID
|
def initialize(thepkt)
|
@setID = thepkt['hdr'].setID
|
end # init
|
def Par2BasePacket.Test(pkt)
|
throw RuntimeError.new("Must override base class!")
|
end # Test
|
def Par2BasePacket.Sig
|
throw RuntimeError.new("Must override base class!")
|
end # Sig
|
end # Par2BasePacket
|
|
class WrongPacketType < RuntimeError; end
|
|
# The File Description Packet
|
class Par2FileDescriptionPacket < Par2BasePacket
|
attr_accessor :fileID, :fileMD5, :fileMD5_16, :fileLength, :fileName
|
def Par2FileDescriptionPacket.Sig
|
"PAR 2.0\0FileDesc"
|
end # Sig
|
def Par2FileDescriptionPacket.Test(thepkt)
|
thepkt['hdr'].type == Sig()
|
end
|
def initialize(thepkt)
|
super(thepkt)
|
throw WrongPacketType.new("Par2FileDescriptionPacket but not valid!") if !Par2FileDescriptionPacket.Test(thepkt)
|
@fileID = thepkt['data'][ 0..15]; # TODO: Need to convert to something useful
|
@fileMD5 = thepkt['data'][16..31]; # TODO: Need to convert to something useful
|
@fileMD5_16 = thepkt['data'][32..47]; # TODO: Need to convert to something useful
|
@fileLength = thepkt['data'][48..55].unpack('Q')[0]
|
@fileName = thepkt['data'][56..-1].chomp("\0").chomp("\0").chomp("\0") # There can be up to 3 NULLs
|
end # init
|
end # Par2FileDescriptionPacket
|
|
########################################################################### End of support objects (old "par2packet.rb")
|
|
begin
|
debug = false
|
procd = {} # Need to keep track; don't want to process files repeatedly. (This may not be needed since we move them out and check existence...)
|
Dir.glob('*.par2', File::FNM_CASEFOLD){|parfile|
|
mylist = Par2FileListing.new(parfile)
|
target = File.basename(parfile, ".par2")
|
target = $1 if target =~ /^(.*)\.par2/i # Above may miss all-caps
|
target = $1 if target =~ /^(.*)\.mp3/i
|
target = $1 if target =~ /^(.*)\.vol/i
|
print "Processing #{target}... ";
|
filecnt = 0
|
FileUtils.mkdir target unless File.exist? target
|
FileUtils.move parfile, target, :verbose => debug
|
mylist.each{|thepkt|
|
if procd[thepkt.fileName].nil?
|
# We have never seen this file before
|
# print "#{parfile}:\n\t" << thepkt.fileName << "\n"
|
if File.exist? thepkt.fileName # Don't crash if file missing
|
procd[thepkt.fileName] = thepkt.setID # Store for later
|
FileUtils.move thepkt.fileName, target, :verbose => debug
|
filecnt = filecnt+1
|
end # exists
|
else # seen it before
|
print "Skipping #{thepkt.fileName} - already processed!\n" if debug
|
next
|
end # diff file
|
} # mylist
|
print "#{filecnt} file(s).\n"
|
} # glob
|
end
|