Class: Omnizip::Formats::SevenZip::Reader

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

Overview

.7z archive reader Provides read-only access to .7z archives

Constant Summary

Constants included from Constants

Constants::MAJOR_VERSION, Constants::MAX_NUM_BONDS, Constants::MAX_NUM_CODERS, Constants::MAX_NUM_PACK_STREAMS, Constants::MINOR_VERSION, Constants::SIGNATURE, Constants::SIGNATURE_SIZE, Constants::START_HEADER_SIZE

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file_path, options = {}) ⇒ Reader

Initialize reader with file path

Parameters:

  • file_path (String)

    Path to .7z file

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

    Reader options

Options Hash (options):

  • :password (String)

    Password for encrypted headers

  • :offset (Integer)

    Offset for embedded archives



22
23
24
25
26
27
28
29
30
31
32
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 22

def initialize(file_path, options = {})
  @file_path = file_path
  @entries = []
  @stream_info = nil
  @split_reader = nil
  @password = options[:password]
  @offset = options[:offset] || 0
  @solid_cache = {}
  @solid_cache_extracted = {}
  @opened = false
end

Instance Attribute Details

#entriesObject (readonly)

Returns the value of attribute entries.



14
15
16
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 14

def entries
  @entries
end

#file_pathObject (readonly)

Returns the value of attribute file_path.



14
15
16
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 14

def file_path
  @file_path
end

#headerObject (readonly)

Returns the value of attribute header.



14
15
16
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 14

def header
  @header
end

#split_readerObject (readonly)

Returns the value of attribute split_reader.



14
15
16
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 14

def split_reader
  @split_reader
end

#stream_infoObject (readonly)

Returns the value of attribute stream_info.



14
15
16
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 14

def stream_info
  @stream_info
end

Instance Method Details

#can_decrypt?Boolean

Check if can decrypt headers (password provided)

Returns:

  • (Boolean)

    true if password available for encrypted headers



178
179
180
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 178

def can_decrypt?
  encrypted? && !@password.nil?
end

#encrypted?Boolean

Check if headers are encrypted

Returns:

  • (Boolean)

    true if headers are encrypted



171
172
173
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 171

def encrypted?
  !@encrypted_header.nil?
end

#extract_all(output_dir) ⇒ Object

Extract all files to directory

Parameters:

  • output_dir (String)

    Destination directory

Raises:

  • (RuntimeError)

    on extraction error



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 138

def extract_all(output_dir)
  ensure_open
  FileUtils.mkdir_p(output_dir)

  real_out = File.realpath(output_dir)

  @entries.each do |entry|
    next if entry.name.nil? || entry.name.empty?

    output_path = File.join(real_out, entry.name)
    expanded = File.expand_path(output_path)
    unless expanded.start_with?("#{real_out}#{File::SEPARATOR}") || expanded == real_out
      raise Omnizip::FormatError,
            "Path traversal detected: #{entry.name}"
    end

    extract_entry(entry.name, output_path)
  end
ensure
  @solid_cache.clear
  @solid_cache_extracted.clear
end

#extract_entry(entry_name, output_path) ⇒ Object

Extract file to output path

Parameters:

  • entry_name (String)

    File name to extract

  • output_path (String)

    Destination path

Raises:

  • (RuntimeError)

    if entry not found or extraction fails



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 97

def extract_entry(entry_name, output_path)
  ensure_open
  # Delegate to split reader if available
  if @split_reader
    @split_reader.extract_entry(entry_name, output_path)
    return
  end

  entry = @entries.find { |e| e.name == entry_name }
  raise "Entry not found: #{entry_name}" unless entry

  # Create directory if needed
  FileUtils.mkdir_p(File.dirname(output_path))

  # Extract file
  if entry.directory?
    FileUtils.mkdir_p(output_path)
  elsif entry.has_stream?
    File.open(@file_path, "rb") do |io|
      # Seek to offset for embedded archives
      io.seek(@offset) if @offset.positive?
      data = extract_entry_data(io, entry)
      File.binwrite(output_path, data)
    end

    # Set timestamp if available
    if entry.mtime
      File.utime(entry.atime || entry.mtime || Time.now,
                 entry.mtime || Time.now,
                 output_path)
    end
  else
    # Empty file
    FileUtils.touch(output_path)
  end
end

#extract_entry_data(io, entry) ⇒ String

Extract raw entry payload from the archive. Public so converters and parallel extractors can reuse the stream-positioning + decompression logic.

Parameters:

Returns:

  • (String)

    Extracted bytes



189
190
191
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 189

def extract_entry_data(io, entry)
  extract_entry_data_private(io, entry)
end

#list_filesArray<Models::FileEntry>

List all files in archive, parsing on first use — a Reader is always usable; #open remains for eager loading

Returns:



87
88
89
90
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 87

def list_files
  ensure_open
  @entries.reject { |e| e.name.nil? || e.name.empty? }
end

#openObject

Open and parse .7z archive

Raises:

  • (RuntimeError)

    if file cannot be opened or parsed



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 37

def open
  # Check if this is a split archive
  if split_archive?
    @split_reader = SplitArchiveReader.new(@file_path)
    @split_reader.open
    @header = @split_reader.header
    @entries = @split_reader.entries
    @stream_info = @split_reader.stream_info
  else
    File.open(@file_path, "rb") do |io|
      # Seek to offset for embedded archives
      io.seek(@offset) if @offset.positive?
      parse_archive(io)
    end
  end
  self
end

#split?Boolean

Check if archive is split

Returns:

  • (Boolean)

    true if split across multiple volumes



58
59
60
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 58

def split?
  @split_reader&.split? || false
end

#total_volumesInteger

Get total number of volumes (for split archives)

Returns:

  • (Integer)

    Number of volumes



65
66
67
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 65

def total_volumes
  @split_reader&.total_volumes || 1
end

#valid?Boolean

Check if archive is valid .7z format

Returns:

  • (Boolean)

    true if valid



164
165
166
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 164

def valid?
  !@header.nil? && @header.valid?
end

#volume_sizeInteger

Get volume size (for split archives)

Returns:

  • (Integer)

    Volume size in bytes



72
73
74
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 72

def volume_size
  @split_reader&.volume_size || File.size(@file_path)
end

#volumesArray<String>

Get list of volumes (for split archives)

Returns:

  • (Array<String>)

    Volume paths



79
80
81
# File 'lib/omnizip/formats/seven_zip/reader.rb', line 79

def volumes
  @split_reader&.volumes || [@file_path]
end