Class: Omnizip::Formats::Xar::Header

Inherits:
Object
  • Object
show all
Includes:
Constants
Defined in:
lib/omnizip/formats/xar/header.rb

Overview

XAR archive header parser and builder

The XAR header is 28 bytes (or 64 bytes for extended format):

  • Bytes 0-3: Magic number (0x78617221 = "xar!")
  • Bytes 4-5: Header size (little-endian)
  • Bytes 6-7: Format version (little-endian)
  • Bytes 8-15: TOC compressed size (big-endian uint64)
  • Bytes 16-23: TOC uncompressed size (big-endian uint64)
  • Bytes 24-27: Checksum algorithm (big-endian uint32)
  • Bytes 28-63: (Optional) Checksum name for CKSUM_OTHER

Constant Summary

Constants included from Constants

Constants::CHECKSUM_ALGORITHMS, Constants::CHECKSUM_NAMES, Constants::CHECKSUM_SIZES, Constants::CKSUM_MD5, Constants::CKSUM_NONE, Constants::CKSUM_OTHER, Constants::CKSUM_SHA1, Constants::COMPRESSION_BZIP2, Constants::COMPRESSION_GZIP, Constants::COMPRESSION_LZMA, Constants::COMPRESSION_MIME_TYPES, Constants::COMPRESSION_NONE, Constants::COMPRESSION_XZ, Constants::DEFAULT_COMPRESSION, Constants::DEFAULT_COMPRESSION_LEVEL, Constants::DEFAULT_FILE_CHECKSUM, Constants::DEFAULT_TOC_CHECKSUM, Constants::HEADER_CKSUM_ALG_OFFSET, Constants::HEADER_MAGIC_OFFSET, Constants::HEADER_SIZE, Constants::HEADER_SIZE_EX, Constants::HEADER_SIZE_OFFSET, Constants::HEADER_TOC_COMPRESSED_OFFSET, Constants::HEADER_TOC_UNCOMPRESSED_OFFSET, Constants::HEADER_VERSION_OFFSET, Constants::MAGIC, Constants::MAGIC_BYTES, Constants::MAX_PATH_LENGTH, Constants::MIME_TYPE_TO_COMPRESSION, Constants::TOC_XML_DECLARATION, Constants::TYPE_BLOCK, Constants::TYPE_CHAR, Constants::TYPE_DIRECTORY, Constants::TYPE_FIFO, Constants::TYPE_FILE, Constants::TYPE_HARDLINK, Constants::TYPE_SOCKET, Constants::TYPE_SYMLINK, Constants::XAR_VERSION

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(magic: MAGIC, header_size: HEADER_SIZE, version: XAR_VERSION, toc_compressed_size: 0, toc_uncompressed_size: 0, checksum_algorithm: CKSUM_SHA1, checksum_name: nil) ⇒ Header

Initialize header

Parameters:

  • magic (Integer) (defaults to: MAGIC)

    Magic number

  • header_size (Integer) (defaults to: HEADER_SIZE)

    Header size in bytes

  • version (Integer) (defaults to: XAR_VERSION)

    Format version

  • toc_compressed_size (Integer) (defaults to: 0)

    Compressed TOC size

  • toc_uncompressed_size (Integer) (defaults to: 0)

    Uncompressed TOC size

  • checksum_algorithm (Integer) (defaults to: CKSUM_SHA1)

    Checksum algorithm constant

  • checksum_name (String, nil) (defaults to: nil)

    Checksum name for CKSUM_OTHER



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/omnizip/formats/xar/header.rb', line 107

def initialize(magic: MAGIC,
               header_size: HEADER_SIZE,
               version: XAR_VERSION,
               toc_compressed_size: 0,
               toc_uncompressed_size: 0,
               checksum_algorithm: CKSUM_SHA1,
               checksum_name: nil)
  @magic = magic
  @header_size = checksum_algorithm == CKSUM_OTHER && checksum_name ? HEADER_SIZE_EX : header_size
  @version = version
  @toc_compressed_size = toc_compressed_size
  @toc_uncompressed_size = toc_uncompressed_size
  @checksum_algorithm = checksum_algorithm
  @checksum_name = checksum_name
end

Instance Attribute Details

#checksum_algorithmObject (readonly)

Returns the value of attribute checksum_algorithm.



19
20
21
# File 'lib/omnizip/formats/xar/header.rb', line 19

def checksum_algorithm
  @checksum_algorithm
end

#checksum_nameObject (readonly)

Returns the value of attribute checksum_name.



19
20
21
# File 'lib/omnizip/formats/xar/header.rb', line 19

def checksum_name
  @checksum_name
end

#header_sizeObject (readonly)

Returns the value of attribute header_size.



19
20
21
# File 'lib/omnizip/formats/xar/header.rb', line 19

def header_size
  @header_size
end

#magicObject (readonly)

Returns the value of attribute magic.



19
20
21
# File 'lib/omnizip/formats/xar/header.rb', line 19

def magic
  @magic
end

#toc_compressed_sizeObject (readonly)

Returns the value of attribute toc_compressed_size.



19
20
21
# File 'lib/omnizip/formats/xar/header.rb', line 19

def toc_compressed_size
  @toc_compressed_size
end

#toc_uncompressed_sizeObject (readonly)

Returns the value of attribute toc_uncompressed_size.



19
20
21
# File 'lib/omnizip/formats/xar/header.rb', line 19

def toc_uncompressed_size
  @toc_uncompressed_size
end

#versionObject (readonly)

Returns the value of attribute version.



19
20
21
# File 'lib/omnizip/formats/xar/header.rb', line 19

def version
  @version
end

Class Method Details

.parse(data) ⇒ Header

Parse header from binary data

Parameters:

  • data (String)

    Binary header data (28+ bytes)

Returns:

  • (Header)

    Parsed header object

Raises:

  • (ArgumentError)

    If data is invalid



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
# File 'lib/omnizip/formats/xar/header.rb', line 28

def self.parse(data)
  if data.bytesize < HEADER_SIZE
    raise ArgumentError,
          "Header data too short (#{data.bytesize} bytes)"
  end

  magic = data[0, 4].unpack1("N")
  # XAR spec: header_size and version are little-endian, rest are big-endian
  # However, some tools (like macOS xar) store these in big-endian format.
  # We detect this by checking if the parsed values make sense.
  header_size_le = data[4, 2].unpack1("v")  # little-endian
  header_size_be = data[4, 2].unpack1("n")  # big-endian
  version_le = data[6, 2].unpack1("v")      # little-endian
  version_be = data[6, 2].unpack1("n")      # big-endian

  # Detect endianness: standard header is 28 bytes, version is 1
  # If little-endian gives valid values, use it; otherwise use big-endian
  if header_size_le == HEADER_SIZE && version_le == XAR_VERSION
    header_size = header_size_le
    version = version_le
  elsif header_size_be == HEADER_SIZE && version_be == XAR_VERSION
    header_size = header_size_be
    version = version_be
  else
    # Default to little-endian (spec-compliant)
    header_size = header_size_le
    version = version_le
    # Normalize version 256 to 1 (big-endian encoding of version 1)
    version = 1 if version == 256
  end
  toc_compressed_size = data[8, 8].unpack1("Q>") # big-endian uint64
  toc_uncompressed_size = data[16, 8].unpack1("Q>") # big-endian uint64
  checksum_algorithm = data[24, 4].unpack1("N")

  # Parse checksum name for custom algorithms
  checksum_name = nil
  if checksum_algorithm == CKSUM_OTHER && data.bytesize >= HEADER_SIZE_EX
    checksum_name = data[28, 36].strip
  end

  new(
    magic: magic,
    header_size: header_size,
    version: version,
    toc_compressed_size: toc_compressed_size,
    toc_uncompressed_size: toc_uncompressed_size,
    checksum_algorithm: checksum_algorithm,
    checksum_name: checksum_name,
  )
end

.read(file) ⇒ Header

Read header from file

Parameters:

  • file (IO)

    File handle positioned at start

Returns:

Raises:

  • (ArgumentError)


83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/omnizip/formats/xar/header.rb', line 83

def self.read(file)
  data = file.read(HEADER_SIZE_EX) # Read max possible size
  raise ArgumentError, "Failed to read header" unless data

  # Parse to get actual header size
  header = parse(data)

  # Seek back if we read too much
  if data.bytesize > header.header_size
    file.seek(header.header_size, ::IO::SEEK_SET)
  end

  header
end

Instance Method Details

#checksum?Boolean

Check if checksum is used

Returns:

  • (Boolean)

    true if checksum is used



185
186
187
# File 'lib/omnizip/formats/xar/header.rb', line 185

def checksum?
  @checksum_algorithm != CKSUM_NONE
end

#checksum_algorithm_nameString

Get checksum algorithm name

Returns:

  • (String)

    Checksum algorithm name



167
168
169
170
171
172
173
# File 'lib/omnizip/formats/xar/header.rb', line 167

def checksum_algorithm_name
  if @checksum_algorithm == CKSUM_OTHER
    @checksum_name || "unknown"
  else
    CHECKSUM_NAMES[@checksum_algorithm] || "unknown"
  end
end

#checksum_sizeInteger

Get checksum size in bytes

Returns:

  • (Integer)

    Checksum size



178
179
180
# File 'lib/omnizip/formats/xar/header.rb', line 178

def checksum_size
  CHECKSUM_SIZES[checksum_algorithm_name] || 0
end

#to_bytesString

Serialize header to binary

Returns:

  • (String)

    Binary header data



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/omnizip/formats/xar/header.rb', line 192

def to_bytes
  data = +""
  data << [@magic].pack("N")                    # 4 bytes, big-endian
  data << [@header_size].pack("v")              # 2 bytes, little-endian
  data << [@version].pack("v")                  # 2 bytes, little-endian
  data << [@toc_compressed_size].pack("Q>")     # 8 bytes, big-endian
  data << [@toc_uncompressed_size].pack("Q>")   # 8 bytes, big-endian
  data << [@checksum_algorithm].pack("N")       # 4 bytes, big-endian

  # Add checksum name for custom algorithms
  if @checksum_algorithm == CKSUM_OTHER && @checksum_name
    name_bytes = @checksum_name.to_s.encode("ASCII").ljust(36, "\x00")
    data << name_bytes[0, 36]
  end

  data
end

#update_toc_sizes(compressed, uncompressed) ⇒ Header

Update TOC sizes

Parameters:

  • compressed (Integer)

    Compressed TOC size

  • uncompressed (Integer)

    Uncompressed TOC size

Returns:

  • (Header)

    self for chaining



215
216
217
218
219
# File 'lib/omnizip/formats/xar/header.rb', line 215

def update_toc_sizes(compressed, uncompressed)
  @toc_compressed_size = compressed
  @toc_uncompressed_size = uncompressed
  self
end

#valid?Boolean

Check if header is valid

Returns:

  • (Boolean)

    true if valid



158
159
160
161
162
# File 'lib/omnizip/formats/xar/header.rb', line 158

def valid?
  validate!
rescue ArgumentError
  false
end

#validate!Boolean

Validate header

Returns:

  • (Boolean)

    true if valid

Raises:

  • (ArgumentError)

    If header is invalid



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/omnizip/formats/xar/header.rb', line 127

def validate!
  unless @magic == MAGIC
    raise ArgumentError,
          format("Invalid magic: 0x%08x (expected 0x%08x)", @magic,
                 MAGIC)
  end

  unless @header_size >= HEADER_SIZE
    raise ArgumentError, "Header size too small: #{@header_size}"
  end

  unless @version == XAR_VERSION
    raise ArgumentError, "Unsupported version: #{@version}"
  end

  unless [CKSUM_NONE, CKSUM_SHA1, CKSUM_MD5,
          CKSUM_OTHER].include?(@checksum_algorithm)
    raise ArgumentError,
          "Unknown checksum algorithm: #{@checksum_algorithm}"
  end

  if @checksum_algorithm == CKSUM_OTHER && @checksum_name.to_s.strip.empty?
    raise ArgumentError, "Custom checksum requires checksum_name"
  end

  true
end