1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 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
|
# frozen_string_literal: true
class Minitar
# Implements the PAX Extended Header as a Ruby class. The header consists of following
# format:
#
# <decimal-length><space><ascii-keyword>=<value><newline>
#
# Where:
#
# - +decimal-length+ is the total number of bytes for the PaxHeader record using ASCII
# decimal values; this includes the terminal newline (0x10).
# - +space+ is a single literal ASCII space (0x20).
# - +ascii-keyword+ is a PAX Extended Header keyword, which may be any ASCII character
# except newline (0x10) or equal sign (0x3D).
# - +=+ is the literal ASCII equal sign (0x3D).
# - +value+ is any series of bytes except newline (0x10).
# - +newline+ is the literal ASCII newline (0x10).
#
# There are several keywords defined in the POSIX standard; some of them are supported
# in this class, but may not be supported by Minitar as a whole.
#
# Primary support for PAX extended headers is for extracting size information for large
# file support. Other features may be added in the future.
class PaxHeader
BLOCK_SIZE = 512
attr_reader :attributes
class << self
# Creates a new PaxHeader from a data stream and posix header. Reads the PAX content
# based on the size specified in the posix header.
def from_stream(stream, posix_header)
raise ArgumentError, "Header must be a PAX header" unless posix_header.pax_header?
pax_block = (posix_header.size / BLOCK_SIZE.to_f).ceil * BLOCK_SIZE
pax_content = stream.read(pax_block)
raise Minitar::InvalidTarStream if pax_content.nil? || pax_content.bytesize < posix_header.size
actual_content = pax_content[0, posix_header.size]
from_data(actual_content)
end
# Creates a new PaxHeader from PAX content data.
def from_data(content) = new(parse_content(content))
private
def parse_content(content)
attributes = {}
offset = 0
while offset < content.bytesize
space_pos = content.index(" ", offset)
break unless space_pos
length_str = content[offset, space_pos - offset]
unless length_str.match?(/\A\d+\z/)
raise ArgumentError, "Invalid length format in PAX header: '#{length_str}'"
end
length = length_str.to_i
if offset + length > content.bytesize
raise ArgumentError, "Length beyond PAX header: '#{content[offset..]}'"
end
record = content[offset, length]
keyword_value = record[(space_pos - offset + 1)..-2]
if keyword_value.include?("=")
keyword, value = keyword_value.split("=", 2)
attributes[keyword] = value
end
offset += length
end
attributes
end
end
# Creates a new PaxHeader from attributes hash.
def initialize(attributes = {})
@attributes = attributes.transform_keys(&:to_s)
end
# The size value from PAX attributes
def size = @attributes["size"]&.to_i
# The path value from PAX attributes
def path = @attributes["path"]
# The mtime value from PAX attributes
def mtime = @attributes["mtime"]&.to_f
# Returns a string representation of the PAX header content.
def to_s
@attributes.map do |keyword, value|
keyword_value = " #{keyword}=#{value}\n"
record = keyword_value
begin
length = record.bytesize
length_str = length.to_s
record = "#{length_str}#{keyword_value}"
end while record.size != length # standard:disable Lint/Loop
record
end.join
end
end
end
|