Class: Omnizip::Formats::Xar::Writer

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

Overview

XAR archive writer

Provides write access to create XAR archives with support for:

  • Multiple compression algorithms (gzip, bzip2, lzma, xz, none)
  • Checksum generation (md5, sha1, sha256, etc.)
  • Extended attributes
  • Hardlinks and symlinks

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(output_path, options = {}) ⇒ Writer

Initialize writer

Parameters:

  • output_path (String)

    Path to output file

  • options (Hash) (defaults to: {})

    Archive options

Options Hash (options):

  • :compression (String)

    Compression algorithm (gzip, bzip2, lzma, xz, none)

  • :compression_level (Integer)

    Compression level (1-9)

  • :toc_checksum (String)

    TOC checksum algorithm (sha1, md5, sha256)

  • :file_checksum (String)

    File checksum algorithm (sha1, md5, sha256)



45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/omnizip/formats/xar/writer.rb', line 45

def initialize(output_path, options = {})
  @output_path = output_path
  @options = {
    compression: DEFAULT_COMPRESSION,
    compression_level: DEFAULT_COMPRESSION_LEVEL,
    toc_checksum: DEFAULT_TOC_CHECKSUM,
    file_checksum: DEFAULT_FILE_CHECKSUM,
  }.merge(options)

  @entries = []
  @heap_data = +""
  @next_id = 1
  @hardlinks = {} # inode -> entry mapping for hardlink detection
end

Instance Attribute Details

#entriesObject (readonly)

Returns the value of attribute entries.



20
21
22
# File 'lib/omnizip/formats/xar/writer.rb', line 20

def entries
  @entries
end

#optionsObject (readonly)

Returns the value of attribute options.



20
21
22
# File 'lib/omnizip/formats/xar/writer.rb', line 20

def options
  @options
end

#output_pathObject (readonly)

Returns the value of attribute output_path.



20
21
22
# File 'lib/omnizip/formats/xar/writer.rb', line 20

def output_path
  @output_path
end

Class Method Details

.create(output_path, options = {}) {|Writer| ... } ⇒ String

Create a XAR archive

Parameters:

  • output_path (String)

    Path to output file

  • options (Hash) (defaults to: {})

    Archive options

Yields:

  • (Writer)

    Writer instance for adding files

Returns:

  • (String)

    Path to created archive



28
29
30
31
32
33
34
35
# File 'lib/omnizip/formats/xar/writer.rb', line 28

def self.create(output_path, options = {})
  writer = new(output_path, options)

  yield writer if block_given?

  writer.write
  output_path
end

Instance Method Details

#add_data(archive_path, data, options = {}) ⇒ Entry

Add entry from data

Parameters:

  • archive_path (String)

    Path in archive

  • data (String)

    File data

  • options (Hash) (defaults to: {})

    Entry options

Returns:

  • (Entry)

    Added entry



151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/omnizip/formats/xar/writer.rb', line 151

def add_data(archive_path, data, options = {})
  entry = Entry.new(archive_path, {
                      type: TYPE_FILE,
                      mode: options[:mode] || 0o644,
                      mtime: options[:mtime] || Time.now,
                      atime: options[:atime] || Time.now,
                      **options,
                    })

  entry.data = data
  compress_and_add_entry_data(entry, data)
  add_entry(entry)
end

#add_directory(path, archive_path = nil) ⇒ Entry

Add directory to archive

Parameters:

  • path (String)

    Directory path (on disk)

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

    Path in archive

Returns:

  • (Entry)

    Added entry



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

def add_directory(path, archive_path = nil)
  archive_path ||= File.basename(path)
  stat = File.stat(path)

  entry = Entry.new(archive_path, {
                      type: TYPE_DIRECTORY,
                      mode: stat.mode & 0o7777,
                      uid: stat.uid,
                      gid: stat.gid,
                      mtime: stat.mtime,
                      atime: stat.atime,
                      ctime: to_time(stat.ctime),
                    })

  add_entry(entry)
end

#add_entry(entry) ⇒ Entry

Add entry to archive

Parameters:

  • entry (Entry)

    Entry to add

Returns:

  • (Entry)

    Added entry



169
170
171
172
173
174
# File 'lib/omnizip/formats/xar/writer.rb', line 169

def add_entry(entry)
  entry.id ||= @next_id
  @next_id = [@next_id, entry.id + 1].max
  @entries << entry
  entry
end

#add_file(path, archive_path = nil) ⇒ Entry

Add file to archive

Parameters:

  • path (String)

    File path (on disk)

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

    Path in archive (defaults to basename)

Returns:

  • (Entry)

    Added entry



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

def add_file(path, archive_path = nil)
  archive_path ||= File.basename(path)
  stat = File.stat(path)

  entry = Entry.new(archive_path, {
                      type: TYPE_FILE,
                      mode: stat.mode & 0o7777,
                      uid: stat.uid,
                      gid: stat.gid,
                      size: stat.size,
                      mtime: stat.mtime,
                      atime: stat.atime,
                      ctime: to_time(stat.ctime),
                    })

  # Check for hardlink
  if stat.nlink > 1
    inode_key = "#{stat.dev}:#{stat.ino}"
    if (existing = @hardlinks[inode_key])
      # Create hardlink entry instead
      entry.type = TYPE_HARDLINK
      entry.link_type = "hard"
      entry.link_target = existing.name
      entry.nlink = stat.nlink
    else
      @hardlinks[inode_key] = entry
      read_and_add_file_data(entry, path)
    end
  else
    read_and_add_file_data(entry, path)
  end

  add_entry(entry)
end

Add symlink to archive

Parameters:

  • path (String)

    Symlink path (on disk)

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

    Path in archive

Returns:

  • (Entry)

    Added entry



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/omnizip/formats/xar/writer.rb', line 127

def add_symlink(path, archive_path = nil)
  archive_path ||= File.basename(path)
  stat = File.lstat(path)

  entry = Entry.new(archive_path, {
                      type: TYPE_SYMLINK,
                      mode: stat.mode & 0o7777,
                      uid: stat.uid,
                      gid: stat.gid,
                      mtime: stat.mtime,
                      atime: stat.atime,
                      link_type: "symbolic",
                      link_target: File.readlink(path),
                    })

  add_entry(entry)
end

#add_tree(path, archive_path = nil) ⇒ Object

Add tree (directory recursively) to archive

Parameters:

  • path (String)

    Root directory path

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

    Base path in archive



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/omnizip/formats/xar/writer.rb', line 180

def add_tree(path, archive_path = nil)
  stat = File.lstat(path)

  if stat.symlink?
    add_symlink(path, archive_path)
  elsif stat.directory?
    add_directory(path, archive_path)

    Dir.foreach(path) do |entry|
      next if [".", ".."].include?(entry)

      child_path = File.join(path, entry)
      child_archive_path = if archive_path
                             File.join(archive_path,
                                       entry)
                           else
                             entry
                           end
      add_tree(child_path, child_archive_path)
    end
  else
    add_file(path, archive_path)
  end
end

#writeString

Write archive to disk

Returns:

  • (String)

    Output path



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/omnizip/formats/xar/writer.rb', line 208

def write
  File.open(@output_path, "wb") do |file|
    # Reserve space for header (will write at end)
    header = Header.new(
      checksum_algorithm: checksum_to_constant(@options[:toc_checksum]),
      checksum_name: checksum_needs_name(@options[:toc_checksum]) ? @options[:toc_checksum] : nil,
    )
    file.write("\x00" * header.header_size)

    # Build TOC with heap offsets
    toc = build_toc

    # Write compressed TOC
    compressed_toc = toc.compress
    toc_uncompressed_size = toc.uncompressed_size
    file.write(compressed_toc)

    # Calculate and write TOC checksum
    file.pos
    toc_checksum_data = compute_checksum(compressed_toc,
                                         @options[:toc_checksum])
    file.write(toc_checksum_data)
    toc_checksum_size = toc_checksum_data.bytesize

    # Update TOC checksum info
    toc.checksum_offset = 0
    toc.checksum_size = toc_checksum_size

    # Write heap data
    file.pos
    file.write(@heap_data)

    # Now write header at beginning
    header = Header.new(
      toc_compressed_size: compressed_toc.bytesize,
      toc_uncompressed_size: toc_uncompressed_size,
      checksum_algorithm: checksum_to_constant(@options[:toc_checksum]),
      checksum_name: checksum_needs_name(@options[:toc_checksum]) ? @options[:toc_checksum] : nil,
    )
    file.seek(0)
    file.write(header.to_bytes)
  end

  @output_path
end