davispuh/ruby-libmspack

View on GitHub
lib/libmspack/mscab.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module LibMsPack
    module MsCab
        module Constants
            # Offset from start of cabinet to the reserved header data (if present).
            MSCAB_HDR_RESV_OFFSET = 0x28
            # Cabinet header flag: cabinet has a predecessor.
            MSCAB_HDR_PREVCAB = 0x01
            # Cabinet header flag: cabinet has a successor.
            MSCAB_HDR_NEXTCAB = 0x02
            # Cabinet header flag: cabinet has reserved header space.
            MSCAB_HDR_RESV = 0x04

            # Compression mode: no compression.
            MSCAB_COMP_NONE = 0
            # Compression mode: MSZIP (deflate) compression.
            MSCAB_COMP_MSZIP = 1
            # Compression mode: Quantum compression.
            MSCAB_COMP_QUANTUM = 2
            # Compression mode: LZX compression.
            MSCAB_COMP_LZX = 3

            # attribute: file is read-only.
            MSCAB_ATTRIB_RDONLY = 0x01
            # attribute: file is hidden.
            MSCAB_ATTRIB_HIDDEN = 0x02
            # attribute: file is an operating system file.
            MSCAB_ATTRIB_SYSTEM = 0x04
            # attribute: file is "archived".
            MSCAB_ATTRIB_ARCH = 0x20
            # attribute: file is an executable program.
            MSCAB_ATTRIB_EXEC = 0x40
            # attribute: filename is UTF8, not ISO-8859-1.
            MSCAB_ATTRIB_UTF_NAME = 0x80

            # CabDecompressor#setParam parameter: search buffer size.
            MSCABD_PARAM_SEARCHBUF = 0
            # CabDecompressor#setParam parameter: repair MS-ZIP streams?
            MSCABD_PARAM_FIXMSZIP = 1
            # CabDecompressor#setParam parameter: size of decompression buffer
            MSCABD_PARAM_DECOMPBUF = 2
        end

        # Returns the compression method used by a folder.
        # @param [Fixnum] compType a MsCabdFolder.comp_type value
        # @return [Fixnum] one of MSCAB_COMP_NONE, MSCAB_COMP_MSZIP, MSCAB_COMP_QUANTUM or MSCAB_COMP_LZX
        def self.MsCabdCompMethod(compType)
            compType & 0x0F
        end

        # Returns the compression level used by a folder.
        # @param [Fixnum] compType a MsCabdFolder.comp_type value
        # @return [Fixnum] the compression level. This is only defined by LZX and Quantum compression
        def self.MsCabdCompLevel(compType)
            ((comp_type) >> 8) & 0x1F
        end

        # A structure which represents a single folder in a cabinet or cabinet set.
        #
        # A folder is a single compressed stream of data. When uncompressed, it holds the data of one or more files. A folder may be split across more than one cabinet.
        class MsCabdFolder < FFI::Struct
            layout({
                :next => MsCabdFolder.ptr,
                :comp_type => :int,
                :num_blocks => :uint
            })

            # A next folder in this cabinet or cabinet set, or nil if this is the final folder.
            #
            # @return [MsCabdFolder, nil]
            def next
                return nil if self[:next].pointer.address.zero?
                self[:next]
            end

            # The compression format used by this folder.
            #
            # `#MsCabdCompMethod` should be used on this field to get the algorithm used. `#MsCabdCompLevel` should be used to get the "compression level".
            #
            # @return [Fixnum]
            def comp_type
                self[:comp_type]
            end

            # The total number of data blocks used by this folder.
            #
            # This includes data blocks present in other files, if this folder spans more than one cabinet.
            #
            # @return [Fixnum]
            def num_blocks
                self[:num_blocks]
            end

            # Returns the compression method used by a folder.
            #
            # @return [Fixnum] one of MSCAB_COMP_NONE, MSCAB_COMP_MSZIP, MSCAB_COMP_QUANTUM or MSCAB_COMP_LZX
            def compressionMethod
                MsCab::MsCabdCompMethod(comp_type)
            end

            # Returns the compression level used by a folder.
            #
            # @return [Fixnum] the compression level. This is only defined by LZX and Quantum compression
            def compressionLevel
                MsCab::MsCabdCompLevel(comp_type)
            end
        end

        # A structure which represents a single file in a cabinet or cabinet set.
        class MsCabdFile < FFI::Struct
            layout({
                :next => MsCabdFile.ptr,
                :filename => :string,
                :length => :uint,
                :attribs => :int,
                :time_h => :char,
                :time_m => :char,
                :time_s => :char,
                :date_d => :char,
                :date_m => :char,
                :date_y => :int,
                :folder => MsCabdFolder.ptr,
                :offset => :uint
            })

            # The next file in the cabinet or cabinet set, or nil if this is the final file.
            #
            # @return [MsCabdFile, nil]
            def next
                return nil if self[:next].pointer.address.zero?
                self[:next]
            end

            # The filename of the file.
            #
            # String of up to 255 bytes in length, it may be in either ISO-8859-1 or UTF8 format, depending on the file attributes.
            #
            # @return [String]
            def filename
                self[:filename]
            end

            # The filename of the file.
            #
            # @return [String]
            def getFilename
                str = filename.dup
                if (attribs & Constants::MSCAB_ATTRIB_UTF_NAME) == Constants::MSCAB_ATTRIB_UTF_NAME
                    str.force_encoding(Encoding::UTF_8)
                else
                    str.force_encoding(Encoding::ISO_8859_1)
                end
                str
            end

            # The uncompressed length of the file, in bytes.
            #
            # @return [Fixnum]
            def length
                self[:length]
            end

            # File attributes.
            #
            # The following attributes are defined:
            #
            # * MSCAB_ATTRIB_RDONLY indicates the file is write protected.
            # * MSCAB_ATTRIB_HIDDEN indicates the file is hidden.
            # * MSCAB_ATTRIB_SYSTEM indicates the file is a operating system file.
            # * MSCAB_ATTRIB_ARCH indicates the file is "archived".
            # * MSCAB_ATTRIB_EXEC indicates the file is an executable program.
            # * MSCAB_ATTRIB_UTF_NAME indicates the filename is in UTF8 format rather than ISO-8859-1.
            #
            # @return [Fixnum]
            def attribs
                self[:attribs]
            end

            # File's last modified datetime.
            #
            # @return [Time]
            def datetime
                Time.gm(self[:date_y], self[:date_m], self[:date_d], self[:time_h], self[:time_m], self[:time_s])
            end

            # Folder that contains this file.
            #
            # @return [MsCabdFolder, nil]
            def folder
                return nil if self[:folder].pointer.address.zero?
                self[:folder]
            end

            # The uncompressed offset of this file in its folder.
            #
            # @return [Fixnum]
            def offset
                self[:offset]
            end
        end

        # A structure which represents a single cabinet file.
        #
        # If this cabinet is part of a merged cabinet set, the files and folders fields are common to all cabinets in the set, and will be identical.
        class MsCabdCabinet < FFI::Struct
            layout({
                :next => MsCabdCabinet.ptr,
                :filename => :string,
                :base_offset => :off_t,
                :length => :uint,
                :prevcab => MsCabdCabinet.ptr,
                :nextcab => MsCabdCabinet.ptr,
                :prevname => :string,
                :nextname => :string,
                :previnfo => :string,
                :nextinfo => :string,
                :files => MsCabdFile.ptr,
                :folders => MsCabdFolder.ptr,
                :set_id => :ushort,
                :set_index => :ushort,
                :header_resv => :ushort,
                :flags => :int
            })

            # The next cabinet in a chained list, if this cabinet was opened with MsCabDecompressor#search
            #
            # May be nil to mark the end of the list.
            #
            # @return [MsCabdCabinet, nil]
            def next
                return nil if self[:next].pointer.address.zero?
                self[:next]
            end

            # The filename of the cabinet.
            #
            # More correctly, the filename of the physical file that the cabinet resides in. This is given by the library user and may be in any format.
            #
            # @return [String]
            def filename
                self[:filename]
            end

            # The file offset of cabinet within the physical file it resides in.
            #
            # @return [Fixnum]
            def base_offset
                self[:base_offset]
            end

            # The length of the cabinet file in bytes.
            #
            # @return [Fixnum]
            def length
                self[:length]
            end

            # The previous cabinet in a cabinet set, or nil.
            #
            # @return [MsCabdCabinet, nil]
            def prevcab
                return nil if self[:prevcab].pointer.address.zero?
                self[:prevcab]
            end

            # The next cabinet in a cabinet set, or nil.
            #
            # @return [MsCabdCabinet, nil]
            def nextcab
                return nil if self[:nextcab].pointer.address.zero?
                self[:nextcab]
            end

            # The filename of the previous cabinet in a cabinet set, or nil.
            #
            # @return [String]
            def prevname
                self[:prevname]
            end

            # The filename of the next cabinet in a cabinet set, or nil.
            #
            # @return [String]
            def nextname
                self[:nextname]
            end

            # The name of the disk containing the previous cabinet in a cabinet set, or nil.
            #
            # @return [String]
            def previnfo
                self[:previnfo]
            end

            # The name of the disk containing the next cabinet in a cabinet set, or nil.
            #
            # @return [String]
            def nextinfo
                self[:nextinfo]
            end

            # A list of all files in the cabinet or cabinet set.
            #
            # @return [MsCabdFile, nil]
            def files
                return nil if self[:files].pointer.address.zero?
                self[:files]
            end

            # A list of all folders in the cabinet or cabinet set.
            #
            # @return [MsCabdFolder, nil]
            def folders
                return nil if self[:folders].pointer.address.zero?
                self[:folders]
            end

            # The set ID of the cabinet.
            #
            # All cabinets in the same set should have the same set ID.
            #
            # @return [Fixnum]
            def set_id
                self[:set_id]
            end

            # The index number of the cabinet within the set.
            #
            # Numbering should start from 0 for the first cabinet in the set, and increment by 1 for each following cabinet.
            #
            # @return [Fixnum]
            def set_index
                self[:set_index]
            end

            # The number of bytes reserved in the header area of the cabinet.
            #
            # If this is non-zero and flags has MSCAB_HDR_RESV set, this data can be read by the calling application. It is of the given length, located at offset (base_offset + MSCAB_HDR_RESV_OFFSET) in the cabinet file.
            #
            # @see #flags flags
            # @return [Fixnum]
            def header_resv
                self[:header_resv]
            end

            # Header flags.
            #
            # * MSCAB_HDR_PREVCAB indicates the cabinet is part of a cabinet set, and has a predecessor cabinet.
            # * MSCAB_HDR_NEXTCAB indicates the cabinet is part of a cabinet set, and has a successor cabinet.
            # * MSCAB_HDR_RESV indicates the cabinet has reserved header space.
            #
            # @see #prevname prevname
            # @see #previnfo previnfo
            # @see #nextname nextname
            # @see #nextinfo nextinfo
            # @see #header_resv header_resv
            # @return [Fixnum]
            def flags
                self[:flags]
            end
        end

        # CAB compressor
        class MsCabCompressor < FFI::Struct
            layout({:dummy => :int})
        end

        # CAB decompressor
        class MsCabDecompressor < FFI::Struct
            layout({
                :open => callback([ MsCabDecompressor.ptr, :string ], MsCabdCabinet.ptr),
                :close => callback([ MsCabDecompressor.ptr, MsCabdCabinet.ptr ], :void),
                :search => callback([ MsCabDecompressor.ptr, :string ], MsCabdCabinet.ptr),
                :append => callback([ MsCabDecompressor.ptr, MsCabdCabinet.ptr, MsCabdCabinet.ptr ], :int),
                :prepend => callback([ MsCabDecompressor.ptr, MsCabdCabinet.ptr, MsCabdCabinet.ptr ], :int),
                :extract => callback([ MsCabDecompressor.ptr, MsCabdFile.ptr, :string ], :int),
                :set_param => callback([ MsCabDecompressor.ptr, :int, :int ], :int),
                :last_error => callback([ MsCabDecompressor.ptr ], :int)
            })

            # Opens a cabinet file and reads its contents.
            #
            # If the file opened is a valid cabinet file, all headers will be read and a MsCabdCabinet structure will be returned, with a full list of folders and files.
            #
            # In the case of an error occuring, nil is returned and the error code is available from #last_error
            #
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @param [String] filename name of the cabinet file
            # @return [MsCabdCabinet, nil]
            # @see #close close
            # @see #search search
            # @see #last_error last_error
            def open(decompressor, filename)
                self[:open].call(decompressor, filename)
            end

            # Closes a previously opened cabinet or cabinet set.
            #
            # This closes a cabinet, all cabinets associated with it via the MsCabdCabinet#next, MsCabdCabinet#prevcab and MsCabdCabinet#nextcab, and all folders and files. All memory used by these entities is freed.
            #
            # The cabinet is now invalid and cannot be used again. All MsCabdFolder and MsCabdFile from that cabinet or cabinet set are also now invalid, and cannot be used again.
            #
            # If the cabinet given was created using #search, it MUST be the cabinet returned by #search and not one of the later cabinet pointers further along the MsCabdCabinet::next chain.
            #
            # If extra cabinets have been added using #append or #prepend, these will all be freed, even if the cabinet given is not the first cabinet in the set. Do NOT #close more than one cabinet in the set.
            #
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @param [MsCabdCabinet] cabinet the cabinet to close
            # @see #open open
            # @see #search search
            # @see #append append
            # @see #prepend prepend
            def close(decompressor, cabinet)
                self[:close].call(decompressor, cabinet)
            end

            # Searches a regular file for embedded cabinets.
            #
            # This opens a normal file with the given filename and will search the entire file for embedded cabinet files
            #
            # If any cabinets are found, the equivalent of #open is called on each potential cabinet file at the offset it was found. All successfully #open'ed cabinets are kept in a list.
            #
            # The first cabinet found will be returned directly as the result of this method. Any further cabinets found will be chained in a list using the MsCabdCabinet#next field.
            #
            # In the case of an error occuring anywhere other than the simulated #open, nil is returned and the error code is available from #last_error
            #
            # If no error occurs, but no cabinets can be found in the file, nil is returned and #last_error returns MSPACK_ERR_OK
            #
            # `#close` should only be called on the result of #search, not on any subsequent cabinets in the MsCabdCabinet#next chain.
            #
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @param [String] filename the filename of the file to search for cabinets
            # @return [MsCabdCabinet, nil]
            # @see #close close
            # @see #open open
            # @see #last_error last_error
            def search(decompressor, filename)
                self[:search].call(decompressor, filename)
            end

            # Appends one MsCabdCabinet to another, forming or extending a cabinet set.
            #
            # This will attempt to append one cabinet to another such that (cab.nextcab == nextcab) && (nextcab.prevcab == cab) and any folders split between the two cabinets are merged.
            #
            # The cabinets MUST be part of a cabinet set -- a cabinet set is a cabinet that spans more than one physical cabinet file on disk -- and must be appropriately matched.
            #
            # It can be determined if a cabinet has further parts to load by examining the MsCabdCabinet.flags field:
            #
            # * if (flags & MSCAB_HDR_PREVCAB) is non-zero, there is a predecessor cabinet to #open and #prepend. Its MS-DOS case-insensitive filename is MsCabdCabinet.prevname
            # * if (flags & MSCAB_HDR_NEXTCAB) is non-zero, there is a successor cabinet to #open and #append. Its MS-DOS case-insensitive filename is MsCabdCabinet.nextname
            #
            # If the cabinets do not match, an error code will be returned. Neither cabinet has been altered, and both should be closed seperately.
            #
            # Files and folders in a cabinet set are a single entity. All cabinets in a set use the same file list, which is updated as cabinets in the set are added.
            #
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @param [MsCabdCabinet] cab the cabinet which will be appended to, predecessor of nextcab
            # @param [MsCabdCabinet] nextcab the cabinet which will be appended, successor of cab
            # @return [Fixnum] an error code, or MSPACK_ERR_OK if successful
            # @see #prepend prepend
            # @see #open open
            # @see #close close
            def append(decompressor, cab, nextcab)
                self[:append].call(decompressor, cab, nextcab)
            end

            # Prepends one MsCabdCabinet to another, forming or extending a cabinet set.
            #
            # This will attempt to prepend one cabinet to another, such that (cab.prevcab == prevcab) && (prevcab.nextcab == cab).
            # In all other respects, it is identical to #append.
            #
            # @see #append #append for the full documentation
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @param [MsCabdCabinet] cab the cabinet which will be prepended to, successor of prevcab
            # @param [MsCabdCabinet] prevcab the cabinet which will be prepended, predecessor of cab
            # @see #append append
            # @see #open open
            # @see #close close
            def prepend(decompressor, cab, prevcab)
                self[:prepend].call(decompressor, cab, prevcab)
            end

            # Extracts a file from a cabinet or cabinet set.
            #
            # This extracts a compressed file in a cabinet and writes it to the given filename.
            #
            # The MS-DOS filename of the file, MsCabdCabinet.filename, is NOT USED by #extract. The caller must examine this MS-DOS filename, copy and change it as necessary, create directories as necessary, and provide the correct filename as a parameter, which will be passed unchanged to the decompressor's MsPackSystem#open
            #
            # If the file belongs to a split folder in a multi-part cabinet set, and not enough parts of the cabinet set have been loaded and appended or prepended, an error will be returned immediately.
            #
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @param [MsCabdFile] cabfile the file to be decompressed
            # @param [String] filename the filename of the file being written to
            # @return [Fixnum] an error code, or MSPACK_ERR_OK if successful
            def extract(decompressor, cabfile, filename)
                self[:extract].call(decompressor, cabfile, filename)
            end

            # Sets a CAB decompression engine parameter.
            #
            # The following parameters are defined:
            #
            # * MSCABD_PARAM_SEARCHBUF: How many bytes should be allocated as a buffer when using #search ? The minimum value is 4. The default value is 32768.
            # * MSCABD_PARAM_FIXMSZIP: If non-zero, #extract will ignore bad checksums and recover from decompression errors in MS-ZIP compressed folders. The default value is 0 (don't recover).
            # * MSCABD_PARAM_DECOMPBUF: How many bytes should be used as an input bit buffer by decompressors? The minimum value is 4. The default value is 4096.
            #
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @param [Fixnum] param the parameter to set
            # @param [Fixnum] value the value to set the parameter to
            # @see #close close
            # @see #open open
            def set_param(decompressor, param, value)
                self[:set_param].call(decompressor, param, value)
            end

            # Returns the error code set by the most recently called method
            # @param [MsCabDecompressor] decompressor MsCabDecompressor instance being called
            # @return [Fixnum] the most recent error code
            # @see #open open
            # @see #search search
            def last_error(decompressor)
                self[:last_error].call(decompressor)
            end

        end

        # CAB Compressor
        class CabCompressor
            attr_reader :Compressor
            # Creates a new CAB compressor.
            #
            # @param [MsPack::MsPackSystem, nil] system is a custom mspack system, or nil to use the default
            def initialize(system = nil)
                @Compressor = nil
                init(system)
            end

            # @private
            def init(system = MsPack::RubyPackSystem)
                raise Exceptions::AlreadyInitializedError if @Compressor
                @Compressor = LibMsPack.CreateCabCompressor(system)
            end

            # Destroys an existing CAB compressor
            def destroy
                raise Exceptions::NotInitializedError unless @Compressor
                LibMsPack.DestroyCabCompressor(@Compressor)
                @Compressor = nil
            end
        end

        # CAB decompressor
        class CabDecompressor
            attr_reader :Decompressor
            # Creates a new CAB decompressor.
            #
            # @param [MsPack::MsPackSystem, nil] system a custom mspack system, or nil to use the default
            def initialize(system = nil)
                @Decompressor = nil
                init(system)
            end

            # @private
            def init(system = MsPack::RubyPackSystem)
                raise Exceptions::AlreadyInitializedError if @Decompressor
                @Decompressor = LibMsPack.CreateCabDecompressor(system)
            end

            # Opens a cabinet file and reads its contents.
            #
            # If the file opened is a valid cabinet file, all headers will be read and a MsCabdCabinet structure will be returned, with a full list of folders and files.
            #
            # In the case of an error occuring, exception will be raised.
            #
            # @param [String] filename name of the cabinet file
            # @return [MsCabdCabinet]
            # @raise [Exceptions::LibMsPackError]
            # @see #close close
            # @see #search search
            def open(filename)
                raise Exceptions::NotInitializedError unless @Decompressor
                cabinet = @Decompressor.open(@Decompressor, filename)
                error = lastError
                raise Exceptions::LibMsPackError.new(error) unless error == LibMsPack::MSPACK_ERR_OK
                cabinet
            end

            # Closes a previously opened cabinet or cabinet set.
            #
            # This closes a cabinet, all cabinets associated with it via the MsCabdCabinet#next, MsCabdCabinet#prevcab and MsCabdCabinet#nextcab, and all folders and files. All memory used by these entities is freed.
            #
            # The cabinet is now invalid and cannot be used again. All MsCabdFolder and MsCabdFile from that cabinet or cabinet set are also now invalid, and cannot be used again.
            #
            # If the cabinet given was created using #search, it MUST be the cabinet returned by #search and not one of the later cabinet pointers further along the MsCabdCabinet::next chain.
            #
            # If extra cabinets have been added using #append or #prepend, these will all be freed, even if the cabinet given is not the first cabinet in the set. Do NOT #close more than one cabinet in the set.
            #
            # @param [MsCabdCabinet] cabinet the cabinet to close
            # @see #open open
            # @see #search search
            # @see #append append
            # @see #prepend prepend
            def close(cabinet)
                raise Exceptions::NotInitializedError unless @Decompressor
                @Decompressor.close(@Decompressor, cabinet)
            end

            # Searches a regular file for embedded cabinets.
            #
            # This opens a normal file with the given filename and will search the entire file for embedded cabinet files
            #
            # If any cabinets are found, the equivalent of #open is called on each potential cabinet file at the offset it was found. All successfully #open'ed cabinets are kept in a list.
            #
            # The first cabinet found will be returned directly as the result of this method. Any further cabinets found will be chained in a list using the MsCabdCabinet#next field.
            #
            # In the case of an error occuring anywhere other than the simulated #open, exception will be raised.
            #
            # If no error occurs, but no cabinets can be found in the file, nil is returned.
            #
            # `#close` should only be called on the result of #search, not on any subsequent cabinets in the MsCabdCabinet#next chain.
            #
            # @param [String] filename the filename of the file to search for cabinets
            # @return [MsCabdCabinet]
            # @raise [Exceptions::LibMsPackError]
            # @see #close close
            # @see #open open
            def search(filename)
                raise Exceptions::NotInitializedError unless @Decompressor
                cabinet = @Decompressor.search(@Decompressor, filename)
                error = lastError
                raise Exceptions::LibMsPackError.new(error) unless error == LibMsPack::MSPACK_ERR_OK
                cabinet
            end

            # Appends one MsCabdCabinet to another, forming or extending a cabinet set.
            #
            # This will attempt to append one cabinet to another such that (cab.nextcab == nextcab) && (nextcab.prevcab == cab) and any folders split between the two cabinets are merged.
            #
            # The cabinets MUST be part of a cabinet set -- a cabinet set is a cabinet that spans more than one physical cabinet file on disk -- and must be appropriately matched.
            #
            # It can be determined if a cabinet has further parts to load by examining the MsCabdCabinet.flags field:
            #
            # * if (flags & MSCAB_HDR_PREVCAB) is non-zero, there is a predecessor cabinet to #open and #prepend. Its MS-DOS case-insensitive filename is MsCabdCabinet.prevname
            # * if (flags & MSCAB_HDR_NEXTCAB) is non-zero, there is a successor cabinet to #open and #append. Its MS-DOS case-insensitive filename is MsCabdCabinet.nextname
            #
            # If the cabinets do not match, an exception will be raised. Neither cabinet has been altered, and both should be closed seperately.
            #
            # Files and folders in a cabinet set are a single entity. All cabinets in a set use the same file list, which is updated as cabinets in the set are added.
            #
            # @param [MsCabdCabinet] cab the cabinet which will be appended to, predecessor of nextcab
            # @param [MsCabdCabinet] nextcab the cabinet which will be appended, successor of cab
            # @raise [Exceptions::LibMsPackError]
            # @see #prepend prepend
            # @see #open open
            # @see #close close
            def append(cab, nextcab)
                raise Exceptions::NotInitializedError unless @Decompressor
                error = @Decompressor.append(@Decompressor, cab, nextcab)
                raise Exceptions::LibMsPackError.new(error) unless error == LibMsPack::MSPACK_ERR_OK
                error
            end

            # Prepends one MsCabdCabinet to another, forming or extending a cabinet set.
            #
            # This will attempt to prepend one cabinet to another, such that (cab.prevcab == prevcab) && (prevcab.nextcab == cab).
            # In all other respects, it is identical to #append.
            #
            # @see #append #append for the full documentation
            # @param [MsCabdCabinet] cab the cabinet which will be prepended to, successor of prevcab
            # @param [MsCabdCabinet] prevcab the cabinet which will be prepended, predecessor of cab
            # @raise [Exceptions::LibMsPackError]
            # @see #append append
            # @see #open open
            # @see #close close
            def prepend(cab, prevcab)
                raise Exceptions::NotInitializedError unless @Decompressor
                error = @Decompressor.prepend(@Decompressor, cab, prevcab)
                raise Exceptions::LibMsPackError.new(error) unless error == LibMsPack::MSPACK_ERR_OK
                error
            end

            # Extracts a file from a cabinet or cabinet set.
            #
            # This extracts a compressed file in a cabinet and writes it to the given filename.
            #
            # The MS-DOS filename of the file, MsCabdCabinet.filename, is NOT USED by #extract. The caller must examine this MS-DOS filename, copy and change it as necessary, create directories as necessary, and provide the correct filename as a parameter, which will be passed unchanged to the decompressor's MsPackSystem#open
            #
            # If the file belongs to a split folder in a multi-part cabinet set, and not enough parts of the cabinet set have been loaded and appended or prepended, an exception will be raised immediately.
            #
            # @param [MsCabdFile] cabfile the file to be decompressed
            # @param [String] filename the filename of the file being written to
            # @raise [Exceptions::LibMsPackError]
            def extract(cabfile, filename)
                raise Exceptions::NotInitializedError unless @Decompressor
                error = @Decompressor.extract(@Decompressor, cabfile, filename)
                raise Exceptions::LibMsPackError.new(error) unless error == LibMsPack::MSPACK_ERR_OK
                error
            end

            # Sets a CAB decompression engine parameter.
            #
            # The following parameters are defined:
            #
            # * MSCABD_PARAM_SEARCHBUF: How many bytes should be allocated as a buffer when using #search ? The minimum value is 4. The default value is 32768.
            # * MSCABD_PARAM_FIXMSZIP: If non-zero, #extract will ignore bad checksums and recover from decompression errors in MS-ZIP compressed folders. The default value is 0 (don't recover).
            # * MSCABD_PARAM_DECOMPBUF: How many bytes should be used as an input bit buffer by decompressors? The minimum value is 4. The default value is 4096.
            #
            # @param [Fixnum] param the parameter to set
            # @param [Fixnum] value the value to set the parameter to
            # @raise [Exceptions::LibMsPackError]
            # @see #close close
            # @see #open open
            def setParam(param, value)
                raise Exceptions::NotInitializedError unless @Decompressor
                error = @Decompressor.set_param(@Decompressor, param, value)
                raise Exceptions::LibMsPackError.new(error) unless error == LibMsPack::MSPACK_ERR_OK
                error
            end

            # Returns the error code set by the most recently called method
            # @return [Fixnum] the most recent error code
            def lastError
                raise Exceptions::NotInitializedError unless @Decompressor
                @Decompressor.last_error(@Decompressor)
            end

            # Destroys an existing CAB decompressor
            def destroy
                raise Exceptions::NotInitializedError unless @Decompressor
                LibMsPack.DestroyCabDecompressor(@Decompressor)
                @Decompressor = nil
            end
        end

    end
end