diff --git a/cpio.rb b/cpio.rb index c4b79ef..a2e0bef 100644 --- a/cpio.rb +++ b/cpio.rb @@ -1,200 +1,4 @@ -class BoundedIO - attr_reader :length - attr_reader :remaining - - def initialize(io, length, &eof_callback) - @io = io - @length = length - @remaining = length - - @eof_callback = eof_callback - @eof = false - end - - def read(size=nil) - return nil if eof? - size = @remaining if size.nil? - data = @io.read(size) - @remaining -= data.bytesize - eof? - data - end - - def sysread(size) - raise EOFError, "end of file reached" if eof? - read(size) - end - - def eof? - return false if @remaining > 0 - return @eof if @eof - - @eof_callback.call - @eof = true - end -end - -module CPIO - FIELDS = [ - :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor, - :devminor, :rdevmajor, :rdevminor, :namesize, :check - ] -end - -class CPIO::ASCIIReader - FIELD_SIZES = { - :magic => 6, - :ino => 8, - :mode => 8, - :uid => 8, - :gid => 8, - :nlink => 8, - :mtime => 8, - :filesize => 8, - :devmajor => 8, - :devminor => 8, - :rdevmajor => 8, - :rdevminor => 8, - :namesize => 8, - :check => 8 - } - HEADER_LENGTH = FIELD_SIZES.reduce(0) { |m, (_, v)| m + v } - HEADER_PACK = FIELD_SIZES.collect { |_, v| "A#{v}" }.join - - FIELD_ORDER = [ - :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor, - :devminor, :rdevmajor, :rdevminor, :namesize, :check - ] - - def initialize(io) - @io = io - end - - private - - def io - @io - end - - def each(&block) - while true - entry = read - break if entry.nil? - # The CPIO format has the end-of-stream marker as a file called "TRAILER!!!" - break if entry.name == "TRAILER!!!" - block.call(entry, entry.file) - verify_correct_read(entry) unless entry.directory? - end - end - - def verify_correct_read(entry) - # Read and throw away the whole file if not read at all. - entry.file.tap do |file| - if file.nil? || file.remaining == 0 - # All OK! :) - elsif file.remaining == file.length - file.read(16384) while !file.eof? - else - # The file was only partially read? This should be an error by the - # user. - consumed = file.length - file.remaining - raise BadState, "Only #{consumed} bytes were read of the #{file.length} byte file: #{entry.name}" - end - end - end - - def read - entry = CPIOEntry.new - header = io.read(HEADER_LENGTH) - return nil if header.nil? - FIELD_ORDER.zip(header.unpack(HEADER_PACK)).each do |field, value| - entry.send("#{field}=", value.to_i(16)) - end - - entry.validate - entry.mtime = Time.at(entry.mtime) - read_name(entry, @io) - read_file(entry, @io) - entry - end - - def read_name(entry, io) - entry.name = io.read(entry.namesize - 1) # - 1 for null terminator - nul = io.read(1) - raise ArgumentError, "Corrupt CPIO or bug? Name null terminator was not null: #{nul.inspect}" if nul != "\0" - padding_data = io.read(padding_name(entry)) - # Padding should be all null bytes - if padding_data != ("\0" * padding_data.bytesize) - raise ArgumentError, "Corrupt CPIO or bug? Name null padding was #{padding_name(entry)} bytes: #{padding_data.inspect}" - end - end - - def read_file(entry, io) - if entry.directory? - entry.file = nil - #read_file_padding(entry, io) - nil - else - entry.file = BoundedIO.new(io, entry.filesize) do - read_file_padding(entry, io) - end - end - end - - def read_file_padding(entry, io) - padding_data = io.read(padding_file(entry)) - if padding_data != ("\0" * padding_data.bytesize) - raise ArgumentError, "Corrupt CPIO or bug? File null padding was #{padding_file(entry)} bytes: #{padding_data.inspect}" - end - end - - def padding_name(entry) - # name padding is padding up to a multiple of 4 after header+namesize - -(HEADER_LENGTH + entry.namesize) % 4 - end - - def padding_file(entry) - (-(HEADER_LENGTH + entry.filesize + 2) % 4) - end - public(:each) -end - -class CPIOEntry - CPIO::FIELDS.each do |field| - attr_accessor field - end - - attr_accessor :name - attr_accessor :file - - DIRECTORY_FLAG = 0040000 - - def validate - raise "Invalid magic #{magic.inspect}" if magic != 0x070701 - raise "Invalid ino #{ino.inspect}" if ino < 0 - raise "Invalid mode #{mode.inspect}" if mode < 0 - raise "Invalid uid #{uid.inspect}" if uid < 0 - raise "Invalid gid #{gid.inspect}" if gid < 0 - raise "Invalid nlink #{nlink.inspect}" if nlink < 0 - raise "Invalid mtime #{mtime.inspect}" if mtime < 0 - raise "Invalid filesize #{filesize.inspect}" if filesize < 0 - raise "Invalid devmajor #{devmajor.inspect}" if devmajor < 0 - raise "Invalid devminor #{devminor.inspect}" if devminor < 0 - raise "Invalid rdevmajor #{rdevmajor.inspect}" if rdevmajor < 0 - raise "Invalid rdevminor #{rdevminor.inspect}" if rdevminor < 0 - raise "Invalid namesize #{namesize.inspect}" if namesize < 0 - raise "Invalid check #{check.inspect}" if check < 0 - end # def validate - - def read(*args) - return nil if directory? - file.read(*args) - end - - def directory? - mode & DIRECTORY_FLAG > 0 - end -end +require "arr-pm/cpio" CPIO::ASCIIReader.new(STDIN).each do |entry, file| puts entry.name diff --git a/lib/arr-pm/cpio.rb b/lib/arr-pm/cpio.rb new file mode 100644 index 0000000..0864a14 --- /dev/null +++ b/lib/arr-pm/cpio.rb @@ -0,0 +1,10 @@ +module CPIO + FIELDS = [ + :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor, + :devminor, :rdevmajor, :rdevminor, :namesize, :check + ] +end + +require "arr-pm/cpio/bounded_io" +require "arr-pm/cpio/entry" +require "arr-pm/cpio/ascii_reader" diff --git a/lib/arr-pm/cpio/ascii_reader.rb b/lib/arr-pm/cpio/ascii_reader.rb new file mode 100644 index 0000000..ec1db9a --- /dev/null +++ b/lib/arr-pm/cpio/ascii_reader.rb @@ -0,0 +1,117 @@ +class CPIO::ASCIIReader + FIELD_SIZES = { + :magic => 6, + :ino => 8, + :mode => 8, + :uid => 8, + :gid => 8, + :nlink => 8, + :mtime => 8, + :filesize => 8, + :devmajor => 8, + :devminor => 8, + :rdevmajor => 8, + :rdevminor => 8, + :namesize => 8, + :check => 8 + } + HEADER_LENGTH = FIELD_SIZES.reduce(0) { |m, (_, v)| m + v } + HEADER_PACK = FIELD_SIZES.collect { |_, v| "A#{v}" }.join + + FIELD_ORDER = [ + :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor, + :devminor, :rdevmajor, :rdevminor, :namesize, :check + ] + + def initialize(io) + @io = io + end + + private + + def io + @io + end + + def each(&block) + while true + entry = read + break if entry.nil? + # The CPIO format has the end-of-stream marker as a file called "TRAILER!!!" + break if entry.name == "TRAILER!!!" + block.call(entry, entry.file) + verify_correct_read(entry) unless entry.directory? + end + end + + def verify_correct_read(entry) + # Read and throw away the whole file if not read at all. + entry.file.tap do |file| + if file.nil? || file.remaining == 0 + # All OK! :) + elsif file.remaining == file.length + file.read(16384) while !file.eof? + else + # The file was only partially read? This should be an error by the + # user. + consumed = file.length - file.remaining + raise BadState, "Only #{consumed} bytes were read of the #{file.length} byte file: #{entry.name}" + end + end + end + + def read + entry = CPIO::Entry.new + header = io.read(HEADER_LENGTH) + return nil if header.nil? + FIELD_ORDER.zip(header.unpack(HEADER_PACK)).each do |field, value| + entry.send("#{field}=", value.to_i(16)) + end + + entry.validate + entry.mtime = Time.at(entry.mtime) + read_name(entry, @io) + read_file(entry, @io) + entry + end + + def read_name(entry, io) + entry.name = io.read(entry.namesize - 1) # - 1 for null terminator + nul = io.read(1) + raise ArgumentError, "Corrupt CPIO or bug? Name null terminator was not null: #{nul.inspect}" if nul != "\0" + padding_data = io.read(padding_name(entry)) + # Padding should be all null bytes + if padding_data != ("\0" * padding_data.bytesize) + raise ArgumentError, "Corrupt CPIO or bug? Name null padding was #{padding_name(entry)} bytes: #{padding_data.inspect}" + end + end + + def read_file(entry, io) + if entry.directory? + entry.file = nil + #read_file_padding(entry, io) + nil + else + entry.file = CPIO::BoundedIO.new(io, entry.filesize) do + read_file_padding(entry, io) + end + end + end + + def read_file_padding(entry, io) + padding_data = io.read(padding_file(entry)) + if padding_data != ("\0" * padding_data.bytesize) + raise ArgumentError, "Corrupt CPIO or bug? File null padding was #{padding_file(entry)} bytes: #{padding_data.inspect}" + end + end + + def padding_name(entry) + # name padding is padding up to a multiple of 4 after header+namesize + -(HEADER_LENGTH + entry.namesize) % 4 + end + + def padding_file(entry) + (-(HEADER_LENGTH + entry.filesize + 2) % 4) + end + public(:each) +end diff --git a/lib/arr-pm/cpio/bounded_io.rb b/lib/arr-pm/cpio/bounded_io.rb new file mode 100644 index 0000000..d07bfe0 --- /dev/null +++ b/lib/arr-pm/cpio/bounded_io.rb @@ -0,0 +1,35 @@ +class CPIO::BoundedIO + attr_reader :length + attr_reader :remaining + + def initialize(io, length, &eof_callback) + @io = io + @length = length + @remaining = length + + @eof_callback = eof_callback + @eof = false + end + + def read(size=nil) + return nil if eof? + size = @remaining if size.nil? + data = @io.read(size) + @remaining -= data.bytesize + eof? + data + end + + def sysread(size) + raise EOFError, "end of file reached" if eof? + read(size) + end + + def eof? + return false if @remaining > 0 + return @eof if @eof + + @eof_callback.call + @eof = true + end +end diff --git a/lib/arr-pm/cpio/entry.rb b/lib/arr-pm/cpio/entry.rb new file mode 100644 index 0000000..7203599 --- /dev/null +++ b/lib/arr-pm/cpio/entry.rb @@ -0,0 +1,36 @@ +class CPIO::Entry + CPIO::FIELDS.each do |field| + attr_accessor field + end + + attr_accessor :name + attr_accessor :file + + DIRECTORY_FLAG = 0040000 + + def validate + raise "Invalid magic #{magic.inspect}" if magic != 0x070701 + raise "Invalid ino #{ino.inspect}" if ino < 0 + raise "Invalid mode #{mode.inspect}" if mode < 0 + raise "Invalid uid #{uid.inspect}" if uid < 0 + raise "Invalid gid #{gid.inspect}" if gid < 0 + raise "Invalid nlink #{nlink.inspect}" if nlink < 0 + raise "Invalid mtime #{mtime.inspect}" if mtime < 0 + raise "Invalid filesize #{filesize.inspect}" if filesize < 0 + raise "Invalid devmajor #{devmajor.inspect}" if devmajor < 0 + raise "Invalid devminor #{devminor.inspect}" if devminor < 0 + raise "Invalid rdevmajor #{rdevmajor.inspect}" if rdevmajor < 0 + raise "Invalid rdevminor #{rdevminor.inspect}" if rdevminor < 0 + raise "Invalid namesize #{namesize.inspect}" if namesize < 0 + raise "Invalid check #{check.inspect}" if check < 0 + end # def validate + + def read(*args) + return nil if directory? + file.read(*args) + end + + def directory? + mode & DIRECTORY_FLAG > 0 + end +end