├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── Makefile ├── README.md ├── arr-pm.gemspec ├── cpio.rb ├── lib ├── arr-pm.rb └── arr-pm │ ├── conflicts.rb │ ├── file.rb │ ├── file │ ├── header.rb │ ├── lead.rb │ └── tag.rb │ ├── namespace.rb │ ├── requires.rb │ └── v2 │ ├── architecture.rb │ ├── error.rb │ ├── format.rb │ ├── header.rb │ ├── header_header.rb │ ├── lead.rb │ ├── package.rb │ ├── tag.rb │ └── type.rb └── spec ├── arr-pm └── v2 │ ├── header_spec.rb │ └── lead_spec.rb ├── fixtures ├── example-1.0-1.x86_64.rpm ├── example.json └── pagure-mirror-5.13.2-5.fc35.noarch.rpm └── rpm └── file_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | .*.sw[a-z] 3 | 4 | # build byproducts 5 | build-*/* 6 | fpm.wiki 7 | *.gem 8 | 9 | # python 10 | *.pyc 11 | 12 | # RVM 13 | .rvmrc 14 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Let's not argue over this... 2 | StringLiterals: 3 | Enabled: false 4 | 5 | # I can't find a reason for raise vs fail. 6 | SignalException: 7 | Enabled: false 8 | 9 | # I can't find a reason to prefer 'map' when 'collect' is what I mean. 10 | # I'm collecting things from a list. Maybe someone can help me understand the 11 | # semantics here. 12 | CollectionMethods: 13 | Enabled: false 14 | 15 | # Why do you even *SEE* trailing whitespace? Because your editor was 16 | # misconfigured to highlight trailing whitespace, right? Maybe turn that off? 17 | # ;) 18 | TrailingWhitespace: 19 | Enabled: false 20 | 21 | # Line length is another weird problem that somehow in the past 40 years of 22 | # computing we don't seem to have solved. It's a display problem :( 23 | LineLength: 24 | Max: 9000 25 | 26 | # %w() vs [ "x", "y", ... ] 27 | # The complaint is on lib/pleaserun/detector.rb's map of OS=>Runner, 28 | # i'll ignore it. 29 | WordArray: 30 | MinSize: 5 31 | 32 | # A 20-line method isn't too bad. 33 | MethodLength: 34 | Max: 20 35 | 36 | # Hash rockets (=>) forever. Why? Not all of my hash keys are static symbols. 37 | HashSyntax: 38 | EnforcedStyle: hash_rockets 39 | 40 | # I prefer explicit return. It makes it clear in the code that the 41 | # code author intended to return a value from a method. 42 | RedundantReturn: 43 | Enabled: false 44 | 45 | # My view on a readable case statement seems to disagree with 46 | # what rubocop wants and it doesn't let me configure it other than 47 | # enable/disable. 48 | CaseIndentation: 49 | Enabled: false 50 | 51 | # module This::Module::Definition is good. 52 | Style/ClassAndModuleChildren: 53 | Enabled: true 54 | EnforcedStyle: compact 55 | 56 | # "in interpolation #{use.some("double quotes is ok")}" 57 | Style/StringLiteralsInInterpolation: 58 | Enabled: true 59 | EnforcedStyle: double_quotes 60 | 61 | # Long-block `if !something ... end` are more readable to me than `unless something ... end` 62 | Style/NegatedIf: 63 | Enabled: false 64 | 65 | Style/NumericLiterals: 66 | MinDigits: 6 67 | 68 | # This kind of style "useless use of %x" assumes code is write-once. 69 | Style/UnneededPercentX: 70 | Enabled: false 71 | 72 | Style/FileName: 73 | Enabled: false 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # v0.0.12 4 | 5 | * Security: Fixed a safety problem which would allow for arbitrary shell execution when invoking `RPM::File#extract` or `RPM::File#files`. (Jordan Sissel, @joernchen; #14, #15) 6 | * This library now has no external dependencies. (Jordan Sissel, #18) 7 | * `RPM::File#extract` now correctly works when the target directory contains spaces or other special characters. (@joernchen, #19) 8 | * Listing files (`RPM::File#files`) no longer requires external tools like `cpio` (Jordan Sissel, #17) 9 | 10 | 11 | # v0.0.11 12 | 13 | * Support Ruby 3.0 (Alexey Morozov, #12) 14 | * Fix bug caused when listing config_files on an rpm with no config files. (Daniel Jay Haskin, #7) 15 | 16 | # Older versions 17 | 18 | Changelog not tracked for older versions. -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Jordan Sissel 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GEMSPEC=$(shell ls *.gemspec) 2 | VERSION=$(shell awk -F\" '/spec.version/ { print $$2 }' $(GEMSPEC)) 3 | NAME=$(shell awk -F\" '/spec.name/ { print $$2 }' $(GEMSPEC)) 4 | GEM=$(NAME)-$(VERSION).gem 5 | 6 | .PHONY: package 7 | package: | $(GEM) 8 | 9 | .PHONY: gem 10 | gem: $(GEM) 11 | 12 | $(GEM): 13 | gem build $(GEMSPEC) 14 | 15 | .PHONY: test-package 16 | test-package: $(GEM) 17 | # Sometimes 'gem build' makes a faulty gem. 18 | gem unpack $(GEM) 19 | rm -rf ftw-$(VERSION)/ 20 | 21 | .PHONY: publish 22 | publish: test-package 23 | gem push $(GEM) 24 | 25 | .PHONY: install 26 | install: $(GEM) 27 | gem install $(GEM) 28 | 29 | .PHONY: clean 30 | clean: 31 | -rm -rf .yardoc $(GEM) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARRRRRRRRRR PM 2 | 3 | RPM reader/writer library written in Ruby. 4 | 5 | It aims to provide [fpm](https://github.com/jordansissel/fpm) with a way to 6 | read and write rpms. 7 | 8 | ## Why not use librpm? 9 | 10 | The API is quite confusing in many places, poorly documented in most. I have 11 | reached out to some CentOS/rpm folks to see what we can do about improving that 12 | API. 13 | 14 | Even still, librpm has dependencies of its own. I want fpm to be able to read 15 | and write RPMs without requiring and endless chain of dependencies and most 16 | especially without requiring root access to get things going. 17 | 18 | Mainly, if I try to build librpm myself, I get this: "configure: error: missing 19 | required NSPR / NSS header" and I'm not the burden of dependency resolution on 20 | fpm users. 21 | 22 | ## API ideas 23 | 24 | It should be possible to do a read+modify+write on an RPM. 25 | 26 | ### Creating an RPM (proposed API) 27 | 28 | rpm = RPM.new 29 | 30 | # requires and conflicts 31 | rpm.requires("name") <= "version" # Something fun-dsl-likef 32 | rpm.requires("name", :<=, "version") # Or maybe something like this 33 | 34 | # provides 35 | rpm.provides("name") 36 | 37 | # Add some files 38 | rpm.files << path 39 | rpm.files << path2 40 | rpm.files << path3 41 | 42 | # Scripts? 43 | rpm.scripts[:preinstall](path_or_script_string) 44 | rpm.scripts[:postinstall](path_or_script_string) 45 | rpm.scripts[:preuninstall](path_or_script_string) 46 | rpm.scripts[:postuninstall](path_or_script_string) 47 | 48 | rpm.write(output_file_name) 49 | 50 | ### Reading an RPM (proposed API) 51 | 52 | rpm = RPM.read(file) 53 | 54 | rpm.requires # Array of RPM::Requires ? 55 | rpm.provides # Array of 'Provides' strings? 56 | rpm.conflicts # Array of RPM::Conflicts ? 57 | rpm.files # Array of RPM::File ? 58 | 59 | Maybe something like: 60 | 61 | rpm.files.each do |file| 62 | # Tags that are defined in rpm header tags 63 | # fileclass filecolors filecontexts filedependsn filedependsx filedevices 64 | # filedigests fileflags filegroupname fileinodes filelangs filelinktos 65 | # filemodes filemtimes filerdevs filesizes fileusername fileverifyflags 66 | # 67 | # frankly, we don't care about most of these. Meaningful ones: 68 | # username, groupname, size, mtime, mode, linkto 69 | 70 | # file.io could give a nice IO-like thing that let you read the file out 71 | # of the rpm 72 | end 73 | -------------------------------------------------------------------------------- /arr-pm.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | files = %x{git ls-files}.split("\n") 3 | 4 | spec.name = "arr-pm" 5 | spec.version = "0.0.12" 6 | spec.summary = "RPM reader and writer library" 7 | spec.description = "This library allows to you to read and write rpm " \ 8 | "packages. Written in pure ruby because librpm is not available " \ 9 | "on all systems" 10 | spec.license = "Apache 2" 11 | 12 | spec.files = files 13 | spec.require_paths << "lib" 14 | spec.bindir = "bin" 15 | 16 | spec.authors = ["Jordan Sissel"] 17 | spec.email = ["jls@semicomplete.com"] 18 | 19 | spec.add_development_dependency "flores", ">0" 20 | spec.add_development_dependency "rspec", ">3.0.0" 21 | spec.add_development_dependency "stud", ">=0.0.23" 22 | spec.add_development_dependency "insist", ">=1.0.0" 23 | #spec.homepage = "..." 24 | end 25 | 26 | -------------------------------------------------------------------------------- /cpio.rb: -------------------------------------------------------------------------------- 1 | class BoundedIO 2 | attr_reader :length 3 | attr_reader :remaining 4 | 5 | def initialize(io, length, &eof_callback) 6 | @io = io 7 | @length = length 8 | @remaining = length 9 | 10 | @eof_callback = eof_callback 11 | @eof = false 12 | end 13 | 14 | def read(size=nil) 15 | return nil if eof? 16 | size = @remaining if size.nil? 17 | data = @io.read(size) 18 | @remaining -= data.bytesize 19 | eof? 20 | data 21 | end 22 | 23 | def sysread(size) 24 | raise EOFError, "end of file reached" if eof? 25 | read(size) 26 | end 27 | 28 | def eof? 29 | return false if @remaining > 0 30 | return @eof if @eof 31 | 32 | @eof_callback.call 33 | @eof = true 34 | end 35 | end 36 | 37 | module CPIO 38 | FIELDS = [ 39 | :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor, 40 | :devminor, :rdevmajor, :rdevminor, :namesize, :check 41 | ] 42 | end 43 | 44 | class CPIO::ASCIIReader 45 | FIELD_SIZES = { 46 | :magic => 6, 47 | :ino => 8, 48 | :mode => 8, 49 | :uid => 8, 50 | :gid => 8, 51 | :nlink => 8, 52 | :mtime => 8, 53 | :filesize => 8, 54 | :devmajor => 8, 55 | :devminor => 8, 56 | :rdevmajor => 8, 57 | :rdevminor => 8, 58 | :namesize => 8, 59 | :check => 8 60 | } 61 | HEADER_LENGTH = FIELD_SIZES.reduce(0) { |m, (_, v)| m + v } 62 | HEADER_PACK = FIELD_SIZES.collect { |_, v| "A#{v}" }.join 63 | 64 | FIELD_ORDER = [ 65 | :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor, 66 | :devminor, :rdevmajor, :rdevminor, :namesize, :check 67 | ] 68 | 69 | def initialize(io) 70 | @io = io 71 | end 72 | 73 | private 74 | 75 | def io 76 | @io 77 | end 78 | 79 | def each(&block) 80 | while true 81 | entry = read 82 | break if entry.nil? 83 | # The CPIO format has the end-of-stream marker as a file called "TRAILER!!!" 84 | break if entry.name == "TRAILER!!!" 85 | block.call(entry, entry.file) 86 | verify_correct_read(entry) unless entry.directory? 87 | end 88 | end 89 | 90 | def verify_correct_read(entry) 91 | # Read and throw away the whole file if not read at all. 92 | entry.file.tap do |file| 93 | if file.nil? || file.remaining == 0 94 | # All OK! :) 95 | elsif file.remaining == file.length 96 | file.read(16384) while !file.eof? 97 | else 98 | # The file was only partially read? This should be an error by the 99 | # user. 100 | consumed = file.length - file.remaining 101 | raise BadState, "Only #{consumed} bytes were read of the #{file.length} byte file: #{entry.name}" 102 | end 103 | end 104 | end 105 | 106 | def read 107 | entry = CPIOEntry.new 108 | header = io.read(HEADER_LENGTH) 109 | return nil if header.nil? 110 | FIELD_ORDER.zip(header.unpack(HEADER_PACK)).each do |field, value| 111 | entry.send("#{field}=", value.to_i(16)) 112 | end 113 | 114 | entry.validate 115 | entry.mtime = Time.at(entry.mtime) 116 | read_name(entry, @io) 117 | read_file(entry, @io) 118 | entry 119 | end 120 | 121 | def read_name(entry, io) 122 | entry.name = io.read(entry.namesize - 1) # - 1 for null terminator 123 | nul = io.read(1) 124 | raise ArgumentError, "Corrupt CPIO or bug? Name null terminator was not null: #{nul.inspect}" if nul != "\0" 125 | padding_data = io.read(padding_name(entry)) 126 | # Padding should be all null bytes 127 | if padding_data != ("\0" * padding_data.bytesize) 128 | raise ArgumentError, "Corrupt CPIO or bug? Name null padding was #{padding_name(entry)} bytes: #{padding_data.inspect}" 129 | end 130 | end 131 | 132 | def read_file(entry, io) 133 | if entry.directory? 134 | entry.file = nil 135 | #read_file_padding(entry, io) 136 | nil 137 | else 138 | entry.file = BoundedIO.new(io, entry.filesize) do 139 | read_file_padding(entry, io) 140 | end 141 | end 142 | end 143 | 144 | def read_file_padding(entry, io) 145 | padding_data = io.read(padding_file(entry)) 146 | if padding_data != ("\0" * padding_data.bytesize) 147 | raise ArgumentError, "Corrupt CPIO or bug? File null padding was #{padding_file(entry)} bytes: #{padding_data.inspect}" 148 | end 149 | end 150 | 151 | def padding_name(entry) 152 | # name padding is padding up to a multiple of 4 after header+namesize 153 | -(HEADER_LENGTH + entry.namesize) % 4 154 | end 155 | 156 | def padding_file(entry) 157 | (-(HEADER_LENGTH + entry.filesize + 2) % 4) 158 | end 159 | public(:each) 160 | end 161 | 162 | class CPIOEntry 163 | CPIO::FIELDS.each do |field| 164 | attr_accessor field 165 | end 166 | 167 | attr_accessor :name 168 | attr_accessor :file 169 | 170 | DIRECTORY_FLAG = 0040000 171 | 172 | def validate 173 | raise "Invalid magic #{magic.inspect}" if magic != 0x070701 174 | raise "Invalid ino #{ino.inspect}" if ino < 0 175 | raise "Invalid mode #{mode.inspect}" if mode < 0 176 | raise "Invalid uid #{uid.inspect}" if uid < 0 177 | raise "Invalid gid #{gid.inspect}" if gid < 0 178 | raise "Invalid nlink #{nlink.inspect}" if nlink < 0 179 | raise "Invalid mtime #{mtime.inspect}" if mtime < 0 180 | raise "Invalid filesize #{filesize.inspect}" if filesize < 0 181 | raise "Invalid devmajor #{devmajor.inspect}" if devmajor < 0 182 | raise "Invalid devminor #{devminor.inspect}" if devminor < 0 183 | raise "Invalid rdevmajor #{rdevmajor.inspect}" if rdevmajor < 0 184 | raise "Invalid rdevminor #{rdevminor.inspect}" if rdevminor < 0 185 | raise "Invalid namesize #{namesize.inspect}" if namesize < 0 186 | raise "Invalid check #{check.inspect}" if check < 0 187 | end # def validate 188 | 189 | def read(*args) 190 | return nil if directory? 191 | file.read(*args) 192 | end 193 | 194 | def directory? 195 | mode & DIRECTORY_FLAG > 0 196 | end 197 | end 198 | 199 | CPIO::ASCIIReader.new(STDIN).each do |entry, file| 200 | puts entry.name 201 | file.read unless entry.directory? 202 | end 203 | -------------------------------------------------------------------------------- /lib/arr-pm.rb: -------------------------------------------------------------------------------- 1 | require "arr-pm/namespace" 2 | require "arr-pm/file" 3 | -------------------------------------------------------------------------------- /lib/arr-pm/conflicts.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "namespace") 2 | require File.join(File.dirname(__FILE__), "requires") 3 | 4 | class RPM::Conflicts < RPM::Requires 5 | end 6 | -------------------------------------------------------------------------------- /lib/arr-pm/file.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "namespace") 2 | require File.join(File.dirname(__FILE__), "file", "header") 3 | require File.join(File.dirname(__FILE__), "file", "lead") 4 | require File.join(File.dirname(__FILE__), "file", "tag") 5 | require "fcntl" 6 | require "shellwords" 7 | 8 | # Much of the code here is derived from knowledge gained by reading the rpm 9 | # source code, but mostly it started making more sense after reading this site: 10 | # http://www.rpm.org/max-rpm/s1-rpm-file-format-rpm-file-format.html 11 | 12 | class RPM::File 13 | attr_reader :file 14 | 15 | FLAG_LESS = (1 << 1) # RPMSENSE_LESS = (1 << 1), 16 | FLAG_GREATER = (1 << 2) # RPMSENSE_GREATER = (1 << 2), 17 | FLAG_EQUAL = (1 << 3) # RPMSENSE_EQUAL = (1 << 3), 18 | 19 | # from rpm/rpmfi.h 20 | FLAG_CONFIG_FILE = (1 << 0) # RPMFILE_CONFIG = (1 << 0) 21 | 22 | def initialize(file) 23 | if file.is_a?(String) 24 | file = File.new(file, "r") 25 | end 26 | @file = file 27 | end # def initialize 28 | 29 | # Return the lead for this rpm 30 | # 31 | # This 'lead' structure is almost entirely deprecated in the RPM file format. 32 | def lead 33 | if @lead.nil? 34 | # Make sure we're at the beginning of the file. 35 | @file.seek(0, IO::SEEK_SET) 36 | @lead = ::RPM::File::Lead.new(@file) 37 | 38 | # TODO(sissel): have 'read' return number of bytes read? 39 | @lead.read 40 | end 41 | return @lead 42 | end # def lead 43 | 44 | # Return the signature header for this rpm 45 | def signature 46 | lead # Make sure we've parsed the lead... 47 | 48 | # If signature_type is not 5 (HEADER_SIGNED_TYPE), no signature. 49 | if @lead.signature_type != Header::HEADER_SIGNED_TYPE 50 | @signature = false 51 | return 52 | end 53 | 54 | if @signature.nil? 55 | @signature = ::RPM::File::Header.new(@file) 56 | @signature.read 57 | 58 | # signature headers are padded up to an 8-byte boundar, details here: 59 | # http://rpm.org/gitweb?p=rpm.git;a=blob;f=lib/signature.c;h=63e59c00f255a538e48cbc8b0cf3b9bd4a4dbd56;hb=HEAD#l204 60 | # Throw away the pad. 61 | @file.read(@signature.length % 8) 62 | end 63 | 64 | return @signature 65 | end # def signature 66 | 67 | # Return the header for this rpm. 68 | def header 69 | signature 70 | 71 | if @header.nil? 72 | @header = ::RPM::File::Header.new(@file) 73 | @header.read 74 | end 75 | return @header 76 | end # def header 77 | 78 | # Returns a file descriptor for the payload. On first invocation, it seeks to 79 | # the start of the payload 80 | def payload 81 | header 82 | if @payload.nil? 83 | @payload = @file.clone 84 | # The payload starts after the lead, signature, and header. Remember the signature has an 85 | # 8-byte boundary-rounding. 86 | end 87 | 88 | @payload.seek(@lead.length + @signature.length + @signature.length % 8 + @header.length, IO::SEEK_SET) 89 | return @payload 90 | end # def payload 91 | 92 | def valid_compressor?(name) 93 | # I scanned rpm's rpmio.c for payload implementation names and found the following. 94 | # sed -rne '/struct FDIO_s \w+ *= *\{/{ n; s/^.*"(\w+)",$/\1/p }' rpmio/rpmio.c 95 | # It's possible this misses some supported rpm payload compressors. 96 | 97 | [ "gzip", "bzip2", "xz", "lzma", "zstd" ].include?(name) 98 | end 99 | 100 | # Extract this RPM to a target directory. 101 | # 102 | # This should have roughly the same effect as: 103 | # 104 | # % rpm2cpio blah.rpm | (cd {target}; cpio -i --make-directories) 105 | def extract(target) 106 | if !File.directory?(target) 107 | raise Errno::ENOENT.new(target) 108 | end 109 | 110 | compressor = tags[:payloadcompressor] 111 | if !valid_compressor?(compressor) 112 | raise "Cannot decompress. This RPM uses an invalid compressor '#{compressor}'" 113 | end 114 | 115 | extractor = IO.popen("#{compressor} -d | (cd #{Shellwords.escape(target)}; cpio -i --quiet --make-directories)", "w") 116 | buffer = "" 117 | begin 118 | buffer.force_encoding("BINARY") 119 | rescue NoMethodError 120 | # Do Nothing 121 | end 122 | payload_fd = payload.clone 123 | loop do 124 | data = payload_fd.read(16384, buffer) 125 | break if data.nil? # eof 126 | extractor.write(data) 127 | end 128 | payload_fd.close 129 | extractor.close 130 | end # def extract 131 | 132 | def tags 133 | if @tags.nil? 134 | @tags = {} 135 | header.tags.each do |tag| 136 | tags[tag.tag] = tag.value 137 | end 138 | end 139 | @tags 140 | end # def taghash 141 | 142 | # Get all relations of a given type to this package. 143 | # 144 | # Examples: 145 | # 146 | # rpm.relation(:require) 147 | # rpm.relation(:conflict) 148 | # rpm.relation(:provide) 149 | # 150 | # In the return array-of-arrays, the elements are: 151 | # [ name (string), operator (string), version (string) ] 152 | # 153 | # operator will be ">=", ">", "=", "<", or "<=" 154 | # 155 | # @return Array of [name, operator, version] 156 | def relation(type) 157 | name = "#{type}name".to_sym 158 | flags = "#{type}flags".to_sym 159 | version = "#{type}version".to_sym 160 | # There is no data if we are missing all 3 tag types (name/flags/version) 161 | # FYI: 'tags.keys' is an array, Array#& does set intersection. 162 | return [] if (tags.keys & [name, flags, version]).size != 3 163 | # Find tags name, flags, and version, and return 164 | # an array of "name operator version" 165 | return tags[name].zip(tags[flags], tags[version]) \ 166 | .reduce([]) { |memo, (n,o,v)| memo << [n, operator(o), v] } 167 | end # def relation 168 | 169 | # Get an array of requires defined in this package. 170 | # 171 | # @return Array of [ [name, operator, version], ... ] 172 | def requires 173 | return relation(:require) 174 | end # def requires 175 | 176 | # Get an array of conflicts defined in this package. 177 | # 178 | # @return Array of [ [name, operator, version], ... ] 179 | def conflicts 180 | return relation(:conflict) 181 | end # def conflicts 182 | 183 | # Get an array of provides defined in this package. 184 | # 185 | # @return Array of [ [name, operator, version], ... ] 186 | def provides 187 | return relation(:provide) 188 | end # def provides 189 | 190 | # Get an array of config files 191 | def config_files 192 | # this stuff seems to be in the 'enum rpmfileAttrs_e' from rpm/rpmfi.h 193 | results = [] 194 | # short-circuit if there's no :fileflags tag 195 | return results unless tags.include?(:fileflags) 196 | if !tags[:fileflags].nil? 197 | tags[:fileflags].each_with_index do |flag, i| 198 | # The :fileflags (and other :file... tags) are an array, in order of 199 | # files in the rpm payload, we want a list of paths of config files. 200 | results << files[i] if mask?(flag, FLAG_CONFIG_FILE) 201 | end 202 | end 203 | return results 204 | end # def config_files 205 | 206 | # List the files in this RPM. 207 | # 208 | # This should have roughly the same effect as: 209 | # 210 | # % rpm2cpio blah.rpm | cpio -it 211 | def files 212 | # RPM stores the file metadata split across multiple tags. 213 | # A single path's filename (with no directories) is stored in the "basename" tag. 214 | # The directory a file lives in is stored in the "dirnames" tag 215 | # We can find out what directory a file is in using the "dirindexes" tag. 216 | # 217 | # We can join each entry of dirnames and basenames to make the full filename. 218 | return tags[:basenames].zip(tags[:dirindexes]).map { |name, i| File.join(tags[:dirnames][i], name) } 219 | end # def files 220 | 221 | def mask?(value, mask) 222 | return (value & mask) == mask 223 | end # def mask? 224 | 225 | def operator(flag) 226 | return "<=" if mask?(flag, FLAG_LESS | FLAG_EQUAL) 227 | return ">=" if mask?(flag, FLAG_GREATER | FLAG_EQUAL) 228 | return "=" if mask?(flag, FLAG_EQUAL) 229 | return "<" if mask?(flag, FLAG_LESS) 230 | return ">" if mask?(flag, FLAG_GREATER) 231 | end # def operator 232 | 233 | public(:extract, :payload, :header, :lead, :signature, :initialize, :requires, :conflicts, :provides) 234 | end # class RPM::File 235 | -------------------------------------------------------------------------------- /lib/arr-pm/file/header.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "namespace")) 2 | require File.join(File.dirname(__FILE__), "tag") 3 | 4 | class RPM::File::Header 5 | attr_reader :tags 6 | attr_reader :length 7 | 8 | attr_accessor :magic # 8-byte string magic 9 | attr_accessor :index_count # rpmlib calls this field 'il' unhelpfully 10 | attr_accessor :data_length # rpmlib calls this field 'dl' unhelpfully 11 | 12 | HEADER_SIGNED_TYPE = 5 13 | HEADER_MAGIC = "\x8e\xad\xe8\x01\x00\x00\x00\x00".force_encoding("BINARY") 14 | 15 | # magic + index_count + data_length 16 | HEADER_HEADER_LENGTH = HEADER_MAGIC.length + 4 + 4 17 | TAG_ENTRY_SIZE = 16 # tag id, type, offset, count == 16 bytes 18 | 19 | def initialize(file) 20 | @file = file 21 | 22 | @inspectables = [:@length, :@index_count, :@data_length] 23 | @tags = [] 24 | end 25 | 26 | def read 27 | # TODO(sissel): update the comments here to reflect learnings about rpm 28 | # internals 29 | # At this point assume we've read and consumed the lead and signature. 30 | #len = @rpm.signature.index_length + @rpm.signature 31 | # 32 | # header size is 33 | # ( @rpm.signature.index_length * size of a header entry ) 34 | # + @rpm.signature.data_length 35 | # 36 | # header 'entries' are an 37 | # int32 (tag id), int32 (tag type), int32 (offset), uint32 (count) 38 | # 39 | # len = sizeof(il) + sizeof(dl) + (il * sizeof(struct entryInfo_s)) + dl; 40 | # See rpm's header.c, the headerLoad method function for reference. 41 | 42 | # Header always starts with HEADER_MAGIC + index_count(2bytes) + 43 | # data_length(2bytes) 44 | data = @file.read(HEADER_HEADER_LENGTH).unpack("a8NN") 45 | # TODO(sissel): @index_count is really a count, rename? 46 | @magic, @index_count, @data_length = data 47 | validate 48 | 49 | @index_size = @index_count * TAG_ENTRY_SIZE 50 | tag_data = @file.read(@index_size) 51 | data = @file.read(@data_length) 52 | 53 | (0 ... @index_count).each do |i| 54 | offset = i * TAG_ENTRY_SIZE 55 | entry_data = tag_data[i * TAG_ENTRY_SIZE, TAG_ENTRY_SIZE] 56 | entry = entry_data.unpack("NNNN") 57 | entry << data 58 | tag = ::RPM::File::Tag.new(*entry) 59 | @tags << tag 60 | end # each index 61 | 62 | @length = HEADER_HEADER_LENGTH + @index_size + @data_length 63 | end # def read 64 | 65 | def write 66 | raise "not implemented yet" 67 | # Sort tags by type (integer value) 68 | # emit all tags in order 69 | # then emit all data segments in same order 70 | end # def write 71 | 72 | def validate 73 | # TODO(sissel): raise real exceptions 74 | if @magic != ::RPM::File::Header::HEADER_MAGIC 75 | raise "Header magic did not match; got #{@magic.inspect}, " \ 76 | "expected #{::RPM::File::Header::HEADER_MAGIC.inspect}" 77 | end 78 | 79 | #if !(0..32).include?(@index_count) 80 | #raise "Invalid 'index_count' value #{@index_count}, expected to be in range [0..32]" 81 | #end 82 | 83 | #if !(0..8192).include?(@data_length) 84 | #raise "Invalid 'data_length' value #{@data_length}, expected to be in range [0..8192]" 85 | #end 86 | end # def validate 87 | 88 | end # class RPM::File::Header 89 | -------------------------------------------------------------------------------- /lib/arr-pm/file/lead.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "namespace")) 2 | 3 | class RPM::File::Lead 4 | 5 | #struct rpmlead { 6 | attr_accessor :magic #unsigned char magic[4]; 7 | attr_accessor :major #unsigned char major; 8 | attr_accessor :minor #unsigned char minor; 9 | attr_accessor :type #short type; 10 | attr_accessor :archnum #short archnum; 11 | attr_accessor :name #char name[66]; 12 | attr_accessor :osnum #short osnum; 13 | attr_accessor :signature_type #short signature_type; 14 | attr_accessor :reserved #char reserved[16]; 15 | #} 16 | 17 | attr_accessor :length 18 | 19 | def initialize(file) 20 | @file = file 21 | @inspectables = [:@major, :@minor, :@length, :@type, :@archnum, :@signature_type, :@reserved, :@osnum] 22 | end 23 | 24 | def type 25 | case @type 26 | when 0 27 | return :binary 28 | when 1 29 | return :source 30 | else 31 | raise "Unknown package 'type' value #{@type}" 32 | end 33 | end # def type 34 | 35 | def read 36 | # Use 'A' here instead of 'a' to trim nulls. 37 | @length = 96 38 | data = @file.read(@length).unpack("A4CCnnA66nnA16") 39 | @magic, @major, @minor, @type, @archnum, @name, \ 40 | @osnum, @signature_type, @reserved = data 41 | 42 | return nil 43 | end # def read 44 | 45 | def write(file) 46 | data = [ @magic, @major, @minor, @type, @archnum, @name, \ 47 | @osnum, @signature_type, @reserved ].pack("a4CCnna66nna16") 48 | file.write(data) 49 | end # def write 50 | end # class RPM::File::Lead 51 | -------------------------------------------------------------------------------- /lib/arr-pm/file/tag.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "namespace")) 2 | 3 | class RPM::File::Tag 4 | 5 | attr_accessor :tag 6 | attr_accessor :type 7 | attr_accessor :offset 8 | attr_accessor :count 9 | attr_accessor :value 10 | 11 | # This data can be found mostly in rpmtag.h 12 | TAG = { 13 | 61 => :headerimage, 14 | 62 => :headersignatures, 15 | 63 => :headerimmutable, 16 | 64 => :headerregions, 17 | 100 => :headeri18ntable, 18 | 256 => :sig_base, 19 | 20 | 257 => :sigsize, 21 | 258 => :siglemd5_1, 22 | 259 => :sigpgp, 23 | 260 => :siglemd5_2, 24 | 261 => :sigmd5, 25 | 262 => :siggpg, 26 | 263 => :sigpgp5, 27 | 264 => :badsha1_1, 28 | 265 => :badsha1_2, 29 | 266 => :pubkeys, 30 | 267 => :dsaheader, 31 | 268 => :rsaheader, 32 | 269 => :sha1header, 33 | 270 => :longsigsize, 34 | 271 => :longarchivesize, 35 | 36 | 1000 => :name, 37 | 1001 => :version, 38 | 1002 => :release, 39 | 1003 => :epoch, 40 | 1004 => :summary, 41 | 1005 => :description, 42 | 1006 => :buildtime, 43 | 1007 => :buildhost, 44 | 1008 => :installtime, 45 | 1009 => :size, 46 | 1010 => :distribution, 47 | 1011 => :vendor, 48 | 1012 => :gif, 49 | 1013 => :xpm, 50 | 1014 => :license, 51 | 1015 => :packager, 52 | 1016 => :group, 53 | 1017 => :changelog, 54 | 1018 => :source, 55 | 1019 => :patch, 56 | 1020 => :url, 57 | 1021 => :os, 58 | 1022 => :arch, 59 | 1023 => :prein, 60 | 1024 => :postin, 61 | 1025 => :preun, 62 | 1026 => :postun, 63 | 1027 => :oldfilenames, 64 | 1028 => :filesizes, 65 | 1029 => :filestates, 66 | 1030 => :filemodes, 67 | 1031 => :fileuids, 68 | 1032 => :filegids, 69 | 1033 => :filerdevs, 70 | 1034 => :filemtimes, 71 | 1035 => :filedigests, 72 | 1036 => :filelinktos, 73 | 1037 => :fileflags, 74 | 1038 => :root, 75 | 1039 => :fileusername, 76 | 1040 => :filegroupname, 77 | 1041 => :exclude, 78 | 1042 => :exclusive, 79 | 1043 => :icon, 80 | 1044 => :sourcerpm, 81 | 1045 => :fileverifyflags, 82 | 1046 => :archivesize, 83 | 1047 => :providename, 84 | 1048 => :requireflags, 85 | 1049 => :requirename, 86 | 1050 => :requireversion, 87 | 1051 => :nosource, 88 | 1052 => :nopatch, 89 | 1053 => :conflictflags, 90 | 1054 => :conflictname, 91 | 1055 => :conflictversion, 92 | 1056 => :defaultprefix, 93 | 1057 => :buildroot, 94 | 1058 => :installprefix, 95 | 1059 => :excludearch, 96 | 1060 => :excludeos, 97 | 1061 => :exclusivearch, 98 | 1062 => :exclusiveos, 99 | 1063 => :autoreqprov, 100 | 1064 => :rpmversion, 101 | 1065 => :triggerscripts, 102 | 1066 => :triggername, 103 | 1067 => :triggerversion, 104 | 1068 => :triggerflags, 105 | 1069 => :triggerindex, 106 | 1079 => :verifyscript, 107 | 1080 => :changelogtime, 108 | 1081 => :changelogname, 109 | 1082 => :changelogtext, 110 | 1083 => :brokenmd5, 111 | 1084 => :prereq, 112 | 1085 => :preinprog, 113 | 1086 => :postinprog, 114 | 1087 => :preunprog, 115 | 1088 => :postunprog, 116 | 1089 => :buildarchs, 117 | 1090 => :obsoletename, 118 | 1091 => :verifyscriptprog, 119 | 1092 => :triggerscriptprog, 120 | 1093 => :docdir, 121 | 1094 => :cookie, 122 | 1095 => :filedevices, 123 | 1096 => :fileinodes, 124 | 1097 => :filelangs, 125 | 1098 => :prefixes, 126 | 1099 => :instprefixes, 127 | 1100 => :triggerin, 128 | 1101 => :triggerun, 129 | 1102 => :triggerpostun, 130 | 1103 => :autoreq, 131 | 1104 => :autoprov, 132 | 1105 => :capability, 133 | 1106 => :sourcepackage, 134 | 1107 => :oldorigfilenames, 135 | 1108 => :buildprereq, 136 | 1109 => :buildrequires, 137 | 1110 => :buildconflicts, 138 | 1111 => :buildmacros, 139 | 1112 => :provideflags, 140 | 1113 => :provideversion, 141 | 1114 => :obsoleteflags, 142 | 1115 => :obsoleteversion, 143 | 1116 => :dirindexes, 144 | 1117 => :basenames, 145 | 1118 => :dirnames, 146 | 1119 => :origdirindexes, 147 | 1120 => :origbasenames, 148 | 1121 => :origdirnames, 149 | 1122 => :optflags, 150 | 1123 => :disturl, 151 | 1124 => :payloadformat, 152 | 1125 => :payloadcompressor, 153 | 1126 => :payloadflags, 154 | 1127 => :installcolor, 155 | 1128 => :installtid, 156 | 1129 => :removetid, 157 | 1130 => :sha1rhn, 158 | 1131 => :rhnplatform, 159 | 1132 => :platform, 160 | 1133 => :patchesname, 161 | 1134 => :patchesflags, 162 | 1135 => :patchesversion, 163 | 1136 => :cachectime, 164 | 1137 => :cachepkgpath, 165 | 1138 => :cachepkgsize, 166 | 1139 => :cachepkgmtime, 167 | 1140 => :filecolors, 168 | 1141 => :fileclass, 169 | 1142 => :classdict, 170 | 1143 => :filedependsx, 171 | 1144 => :filedependsn, 172 | 1145 => :dependsdict, 173 | 1146 => :sourcepkgid, 174 | 1147 => :filecontexts, 175 | 1148 => :fscontexts, 176 | 1149 => :recontexts, 177 | 1150 => :policies, 178 | 1151 => :pretrans, 179 | 1152 => :posttrans, 180 | 1153 => :pretransprog, 181 | 1154 => :posttransprog, 182 | 1155 => :disttag, 183 | 1156 => :suggestsname, 184 | 1157 => :suggestsversion, 185 | 1158 => :suggestsflags, 186 | 1159 => :enhancesname, 187 | 1160 => :enhancesversion, 188 | 1161 => :enhancesflags, 189 | 1162 => :priority, 190 | 1163 => :cvsid, 191 | 1164 => :blinkpkgid, 192 | 1165 => :blinkhdrid, 193 | 1166 => :blinknevra, 194 | 1167 => :flinkpkgid, 195 | 1168 => :flinkhdrid, 196 | 1169 => :flinknevra, 197 | 1170 => :packageorigin, 198 | 1171 => :triggerprein, 199 | 1172 => :buildsuggests, 200 | 1173 => :buildenhances, 201 | 1174 => :scriptstates, 202 | 1175 => :scriptmetrics, 203 | 1176 => :buildcpuclock, 204 | 1177 => :filedigestalgos, 205 | 1178 => :variants, 206 | 1179 => :xmajor, 207 | 1180 => :xminor, 208 | 1181 => :repotag, 209 | 1182 => :keywords, 210 | 1183 => :buildplatforms, 211 | 1184 => :packagecolor, 212 | 1185 => :packageprefcolor, 213 | 1186 => :xattrsdict, 214 | 1187 => :filexattrsx, 215 | 1188 => :depattrsdict, 216 | 1189 => :conflictattrsx, 217 | 1190 => :obsoleteattrsx, 218 | 1191 => :provideattrsx, 219 | 1192 => :requireattrsx, 220 | 1193 => :buildprovides, 221 | 1194 => :buildobsoletes, 222 | 1195 => :dbinstance, 223 | 1196 => :nvra, 224 | 5000 => :filenames, 225 | 5001 => :fileprovide, 226 | 5002 => :filerequire, 227 | 5003 => :fsnames, 228 | 5004 => :fssizes, 229 | 5005 => :triggerconds, 230 | 5006 => :triggertype, 231 | 5007 => :origfilenames, 232 | 5008 => :longfilesizes, 233 | 5009 => :longsize, 234 | 5010 => :filecaps, 235 | 5011 => :filedigestalgo, 236 | 5012 => :bugurl, 237 | 5013 => :evr, 238 | 5014 => :nvr, 239 | 5015 => :nevr, 240 | 5016 => :nevra, 241 | 5017 => :headercolor, 242 | 5018 => :verbose, 243 | 5019 => :epochnum, 244 | } 245 | 246 | # See 'rpmTagType' enum in rpmtag.h 247 | TYPE = { 248 | 0 => :null, 249 | 1 => :char, 250 | 2 => :int8, 251 | 3 => :int16, 252 | 4 => :int32, 253 | 5 => :int64, 254 | 6 => :string, 255 | 7 => :binary, 256 | 8 => :string_array, 257 | 9 => :i18nstring, 258 | } 259 | 260 | def initialize(tag_id, type, offset, count, data) 261 | @tag = tag_id 262 | @type = type 263 | @offset = offset 264 | @count = count 265 | 266 | @data = data 267 | 268 | @inspectables = [:@tag, :@type, :@offset, :@count, :@value] 269 | end # def initialize 270 | 271 | def tag 272 | TAG.fetch(@tag, @tag) 273 | end # def tag 274 | 275 | def tag_as_int 276 | @tag 277 | end 278 | 279 | def type 280 | TYPE.fetch(@type, @type) 281 | end # def type 282 | 283 | def value 284 | if !@value 285 | case type 286 | when :string 287 | # string at offset up to first null 288 | @value = @data[@offset .. -1][/^[^\0]+/] 289 | when :i18nstring 290 | # string at offset up to first null 291 | @value = @data[@offset .. -1][/^[^\0]+/] 292 | when :string_array 293 | @value = @data[@offset .. -1].split("\0")[0 ... @count] 294 | when :binary 295 | @value = @data[@offset, @count] 296 | when :int32 297 | @value = @data[@offset, 4 * count].unpack("N" * count) 298 | when :int16 299 | @value = @data[@offset, 2 * count].unpack("n" * count) 300 | end # case type 301 | end # if !@value 302 | 303 | return @value 304 | end # def value 305 | end # class RPM::File::Tag 306 | -------------------------------------------------------------------------------- /lib/arr-pm/namespace.rb: -------------------------------------------------------------------------------- 1 | class RPM 2 | class File; end 3 | end 4 | 5 | module ArrPM 6 | module V2 7 | class Package; end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/arr-pm/requires.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "namespace") 2 | 3 | class RPM::Requires 4 | private 5 | def initialize(name) 6 | @name = name 7 | @version = "0" 8 | @operator = ">=" 9 | end # def initialize 10 | 11 | def condition(operator, version) 12 | @operator = operator 13 | @version = version 14 | end # def condition 15 | 16 | def <=(version); condition(:<=, version) end 17 | def >=(version); condition(:>=, version) end 18 | def <(version); condition(:<, version) end 19 | def >(version); condition(:>, version) end 20 | def ==(version); condition(:==, version) end 21 | 22 | public(:initialize, :<=, :>=, :<, :>, :==) 23 | end 24 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/architecture.rb: -------------------------------------------------------------------------------- 1 | require "arr-pm/namespace" 2 | 3 | module ArrPM::V2::Architecture 4 | 5 | NOARCH = 0 6 | I386 = 1 7 | ALPHA = 2 8 | SPARC = 3 9 | MIPS = 4 10 | PPC = 5 11 | M68K = 6 12 | IP = 7 13 | RS6000 = 8 14 | IA64 = 9 15 | SPARC64 = 10 16 | MIPSEL = 11 17 | ARM = 12 18 | MK68KMINT = 13 19 | S390 = 14 20 | S390X = 15 21 | PPC64 = 16 22 | SH = 17 23 | XTENSA = 18 24 | X86_64 = 19 25 | 26 | module_function 27 | 28 | # Is a given rpm architecture value valid? 29 | def valid?(value) 30 | return value >= 0 && value <= 19 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/error.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "arr-pm/namespace" 4 | require "arr-pm/v2/format" 5 | 6 | module ArrPM::V2::Error 7 | class Base < StandardError; end 8 | 9 | class InvalidMagicValue < Base 10 | def initialize(value) 11 | super("Got invalid magic value '#{value}'. Expected #{ArrPM::V2::Format::MAGIC}.") 12 | end 13 | end 14 | 15 | class InvalidHeaderMagicValue < Base 16 | def initialize(value) 17 | super("Got invalid magic value '#{value}'. Expected #{ArrPM::V2::HeaderHeader::MAGIC}.") 18 | end 19 | end 20 | 21 | class EmptyFile < Base; end 22 | class ShortFile < Base; end 23 | class InvalidVersion < Base; end 24 | class InvalidType < Base 25 | def initialize(value) 26 | super("Invalid type: #{value.inspect}") 27 | end 28 | end 29 | class InvalidName < Base; end 30 | class InvalidArchitecture < Base; end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/format.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "arr-pm/namespace" 4 | 5 | module ArrPM::V2::Format 6 | MAGIC = [0x8e, 0xad, 0xe8] 7 | MAGIC_LENGTH = MAGIC.count 8 | MAGIC_STRING = MAGIC.pack("C#{MAGIC_LENGTH}") 9 | 10 | module_function 11 | def valid_magic?(magic) 12 | magic = magic.bytes if magic.is_a?(String) 13 | 14 | magic == MAGIC 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/header.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "arr-pm/namespace" 4 | require "arr-pm/v2/format" 5 | require "arr-pm/v2/header_header" 6 | require "arr-pm/v2/tag" 7 | require "arr-pm/v2/error" 8 | 9 | class ArrPM::V2::Header 10 | attr_reader :tags 11 | 12 | def load(io) 13 | headerheader = ArrPM::V2::HeaderHeader.new 14 | headerheader.load(io) 15 | headerdata = io.read(headerheader.entries * 16) 16 | tagdata = io.read(headerheader.bytesize) 17 | parse(headerdata, headerheader.entries, tagdata) 18 | 19 | # signature headers are padded up to an 8-byte boundar, details here: 20 | # http://rpm.org/gitweb?p=rpm.git;a=blob;f=lib/signature.c;h=63e59c00f255a538e48cbc8b0cf3b9bd4a4dbd56;hb=HEAD#l204 21 | # Throw away the pad. 22 | io.read(tagdata.length % 8) 23 | end 24 | 25 | def parse(data, entry_count, tagdata) 26 | @tags = entry_count.times.collect do |i| 27 | tag_number, type_number, offset, count = data[i * 16, 16].unpack("NNNN") 28 | 29 | tag = ArrPM::V2::Tag.new(tag_number, type_number) 30 | tag.parse(tagdata, offset, count) 31 | tag 32 | end 33 | nil 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/header_header.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "arr-pm/namespace" 4 | require "arr-pm/v2/format" 5 | require "arr-pm/v2/error" 6 | 7 | # The header of an rpm has ... a header. Funky naming :) 8 | class ArrPM::V2::HeaderHeader 9 | MAGIC = [ 0x8e, 0xad, 0xe8 ] 10 | MAGIC_LENGTH = MAGIC.count 11 | 12 | attr_accessor :version, :entries, :bytesize 13 | 14 | def load(io) 15 | data = io.read(16) 16 | parse(data) 17 | end 18 | 19 | def parse(data) 20 | magic, version, reserved, entries, bytesize = data.unpack("a3Ca4NN") 21 | self.class.validate_magic(magic.bytes) 22 | 23 | @version = version 24 | @entries = entries 25 | @bytesize = bytesize 26 | nil 27 | end 28 | 29 | def dump 30 | [magic, 1, 0, @entries, @bytesize].pack("a3Ca4NN") 31 | end 32 | 33 | def self.validate_magic(value) 34 | raise ArrPM::V2::Error::InvalidHeaderMagicValue, value if value != MAGIC 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/lead.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "arr-pm/namespace" 4 | require "arr-pm/v2/format" 5 | require "arr-pm/v2/error" 6 | 7 | class ArrPM::V2::Lead 8 | LENGTH = 96 9 | MAGIC = [ 0xed, 0xab, 0xee, 0xdb ] 10 | MAGIC_LENGTH = MAGIC.count 11 | 12 | SIGNED_TYPE = 5 13 | 14 | attr_accessor :major, :minor, :type, :architecture, :name, :os, :signature_type, :reserved 15 | 16 | def validate 17 | self.class.validate_type(type) 18 | self.class.validate_architecture(architecture) 19 | if name.length > 65 20 | raise ArrPM::V2::Error::InvalidName, "Name is longer than 65 chracters. This is invalid." 21 | end 22 | end 23 | 24 | def dump(io) 25 | io.write(serialize) 26 | end 27 | 28 | def serialize 29 | validate 30 | [ *MAGIC, major, minor, type, architecture, name, os, signature_type, *reserved ].pack("C4CCnnZ66nnC16") 31 | end 32 | 33 | def load(io) 34 | data = io.read(LENGTH) 35 | parse(data) 36 | end 37 | 38 | def parse(bytestring) 39 | raise ArrPM::V2::Error::EmptyFile if bytestring.nil? 40 | data = bytestring.bytes 41 | 42 | @magic = self.class.parse_magic(data) 43 | @major, @minor = self.class.parse_version(data) 44 | @type = self.class.parse_type(data) 45 | @architecture = self.class.parse_architecture(data) 46 | @name = self.class.parse_name(data) 47 | @os = self.class.parse_os(data) 48 | @signature_type = self.class.parse_signature_type(data) 49 | @reserved = self.class.parse_reserved(data) 50 | self 51 | end 52 | 53 | def signature? 54 | @signature_type == SIGNED_TYPE 55 | end 56 | 57 | def self.valid_version?(version) 58 | version == 1 59 | end 60 | 61 | def self.parse_magic(data) 62 | magic = data[0, MAGIC_LENGTH] 63 | validate_magic(magic) 64 | magic 65 | end 66 | 67 | def self.validate_magic(magic) 68 | raise ArrPM::V2::Error::InvalidMagicValue, magic unless magic == MAGIC 69 | end 70 | 71 | def self.parse_version(data) 72 | offset = MAGIC_LENGTH 73 | major, minor = data[offset, 2] 74 | return major, minor 75 | end 76 | 77 | def self.parse_type(data) 78 | offset = MAGIC_LENGTH + 2 79 | type = data[offset, 2].pack("CC").unpack("n").first 80 | validate_type(type) 81 | type 82 | end 83 | 84 | def self.validate_type(type) 85 | raise ArrPM::V2::Error::InvalidType, type unless ArrPM::V2::Type.valid?(type) 86 | end 87 | 88 | def self.parse_architecture(data) 89 | offset = MAGIC_LENGTH + 4 90 | architecture = data[offset, 2].pack("C*").unpack("n").first 91 | validate_architecture(architecture) 92 | architecture 93 | end 94 | 95 | def self.validate_architecture(architecture) 96 | raise ArrPM::V2::Error::InvalidArchitecture unless ArrPM::V2::Architecture.valid?(architecture) 97 | end 98 | 99 | def self.parse_name(data) 100 | offset = MAGIC_LENGTH + 6 101 | name = data[offset, 66] 102 | length = name.find_index(0) # find the first null byte 103 | raise ArrPM::V2::Error::InvalidName unless length 104 | return name[0, length].pack("C*") 105 | end 106 | 107 | def self.parse_os(data) 108 | offset = MAGIC_LENGTH + 72 109 | data[offset, 2].pack("C*").unpack("n").first 110 | end 111 | 112 | def self.parse_signature_type(data) 113 | offset = MAGIC_LENGTH + 74 114 | data[offset, 2].pack("C*").unpack("n").first 115 | end 116 | 117 | def self.parse_reserved(data) 118 | offset = MAGIC_LENGTH + 76 119 | data[offset, 16] 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/package.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "arr-pm/namespace" 4 | 5 | class ArrPM::V2::RPM 6 | attr_accessor :name 7 | attr_accessor :epoch 8 | attr_accessor :version 9 | attr_accessor :release 10 | 11 | def initialize 12 | defaults 13 | end 14 | 15 | def defaults 16 | @type = Type::BINARY 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/tag.rb: -------------------------------------------------------------------------------- 1 | require "arr-pm/namespace" 2 | 3 | class ArrPM::V2::Tag 4 | module Type 5 | NULL = 0 6 | CHAR = 1 7 | INT8 = 2 8 | INT16 = 3 9 | INT32 = 4 10 | INT64 = 5 11 | STRING = 6 12 | BINARY = 7 13 | STRING_ARRAY = 8 14 | I18NSTRING = 9 15 | 16 | TYPE_MAP = Hash[constants.collect { |c| [const_get(c), c] }] 17 | 18 | def self.parse(data, type, offset, count) 19 | case type 20 | when NULL 21 | nil 22 | when CHAR 23 | data[offset, count].unpack("A#{count}") 24 | when INT8 25 | data[offset, count].unpack("C" * count) 26 | when INT16 27 | data[offset, 2 * count].unpack("n" * count) 28 | when INT32 29 | data[offset, 4 * count].unpack("N" * count) 30 | when INT64 31 | a, b = data[offset, 8].unpack("NN") 32 | a << 32 + b 33 | when STRING, I18NSTRING 34 | data[offset..-1][/^[^\0]*/] 35 | when BINARY 36 | data[offset, count] 37 | when STRING_ARRAY 38 | data[offset..-1].split("\0")[0...count] 39 | else 40 | raise ArrPM::V2::Error::InvalidType, type 41 | end 42 | end 43 | end # module Type 44 | 45 | HEADERIMAGE = 61 46 | HEADERSIGNATURES = 62 47 | HEADERIMMUTABLE = 63 48 | HEADERREGIONS = 64 49 | HEADERI18NTABLE = 100 50 | SIG_BASE = 256 51 | 52 | SIGSIZE = 257 53 | SIGLEMD5_1 = 258 54 | SIGPGP = 259 55 | SIGLEMD5_2 = 260 56 | SIGMD5 = 261 57 | SIGGPG = 262 58 | SIGPGP5 = 263 59 | BADSHA1_1 = 264 60 | BADSHA1_2 = 265 61 | PUBKEYS = 266 62 | DSAHEADER = 267 63 | RSAHEADER = 268 64 | SHA1HEADER = 269 65 | LONGSIGSIZE = 270 66 | LONGARCHIVESIZE = 271 67 | 68 | NAME = 1000 69 | VERSION = 1001 70 | RELEASE = 1002 71 | EPOCH = 1003 72 | SUMMARY = 1004 73 | DESCRIPTION = 1005 74 | BUILDTIME = 1006 75 | BUILDHOST = 1007 76 | INSTALLTIME = 1008 77 | SIZE = 1009 78 | DISTRIBUTION = 1010 79 | VENDOR = 1011 80 | GIF = 1012 81 | XPM = 1013 82 | LICENSE = 1014 83 | PACKAGER = 1015 84 | GROUP = 1016 85 | CHANGELOG = 1017 86 | SOURCE = 1018 87 | PATCH = 1019 88 | URL = 1020 89 | OS = 1021 90 | ARCH = 1022 91 | PREIN = 1023 92 | POSTIN = 1024 93 | PREUN = 1025 94 | POSTUN = 1026 95 | OLDFILENAMES = 1027 96 | FILESIZES = 1028 97 | FILESTATES = 1029 98 | FILEMODES = 1030 99 | FILEUIDS = 1031 100 | FILEGIDS = 1032 101 | FILERDEVS = 1033 102 | FILEMTIMES = 1034 103 | FILEDIGESTS = 1035 104 | FILELINKTOS = 1036 105 | FILEFLAGS = 1037 106 | ROOT = 1038 107 | FILEUSERNAME = 1039 108 | FILEGROUPNAME = 1040 109 | EXCLUDE = 1041 110 | EXCLUSIVE = 1042 111 | ICON = 1043 112 | SOURCERPM = 1044 113 | FILEVERIFYFLAGS = 1045 114 | ARCHIVESIZE = 1046 115 | PROVIDENAME = 1047 116 | REQUIREFLAGS = 1048 117 | REQUIRENAME = 1049 118 | REQUIREVERSION = 1050 119 | NOSOURCE = 1051 120 | NOPATCH = 1052 121 | CONFLICTFLAGS = 1053 122 | CONFLICTNAME = 1054 123 | CONFLICTVERSION = 1055 124 | DEFAULTPREFIX = 1056 125 | BUILDROOT = 1057 126 | INSTALLPREFIX = 1058 127 | EXCLUDEARCH = 1059 128 | EXCLUDEOS = 1060 129 | EXCLUSIVEARCH = 1061 130 | EXCLUSIVEOS = 1062 131 | AUTOREQPROV = 1063 132 | RPMVERSION = 1064 133 | TRIGGERSCRIPTS = 1065 134 | TRIGGERNAME = 1066 135 | TRIGGERVERSION = 1067 136 | TRIGGERFLAGS = 1068 137 | TRIGGERINDEX = 1069 138 | VERIFYSCRIPT = 1079 139 | CHANGELOGTIME = 1080 140 | CHANGELOGNAME = 1081 141 | CHANGELOGTEXT = 1082 142 | BROKENMD5 = 1083 143 | PREREQ = 1084 144 | PREINPROG = 1085 145 | POSTINPROG = 1086 146 | PREUNPROG = 1087 147 | POSTUNPROG = 1088 148 | BUILDARCHS = 1089 149 | OBSOLETENAME = 1090 150 | VERIFYSCRIPTPROG = 1091 151 | TRIGGERSCRIPTPROG = 1092 152 | DOCDIR = 1093 153 | COOKIE = 1094 154 | FILEDEVICES = 1095 155 | FILEINODES = 1096 156 | FILELANGS = 1097 157 | PREFIXES = 1098 158 | INSTPREFIXES = 1099 159 | TRIGGERIN = 1100 160 | TRIGGERUN = 1101 161 | TRIGGERPOSTUN = 1102 162 | AUTOREQ = 1103 163 | AUTOPROV = 1104 164 | CAPABILITY = 1105 165 | SOURCEPACKAGE = 1106 166 | OLDORIGFILENAMES = 1107 167 | BUILDPREREQ = 1108 168 | BUILDREQUIRES = 1109 169 | BUILDCONFLICTS = 1110 170 | BUILDMACROS = 1111 171 | PROVIDEFLAGS = 1112 172 | PROVIDEVERSION = 1113 173 | OBSOLETEFLAGS = 1114 174 | OBSOLETEVERSION = 1115 175 | DIRINDEXES = 1116 176 | BASENAMES = 1117 177 | DIRNAMES = 1118 178 | ORIGDIRINDEXES = 1119 179 | ORIGBASENAMES = 1120 180 | ORIGDIRNAMES = 1121 181 | OPTFLAGS = 1122 182 | DISTURL = 1123 183 | PAYLOADFORMAT = 1124 184 | PAYLOADCOMPRESSOR = 1125 185 | PAYLOADFLAGS = 1126 186 | INSTALLCOLOR = 1127 187 | INSTALLTID = 1128 188 | REMOVETID = 1129 189 | SHA1RHN = 1130 190 | RHNPLATFORM = 1131 191 | PLATFORM = 1132 192 | PATCHESNAME = 1133 193 | PATCHESFLAGS = 1134 194 | PATCHESVERSION = 1135 195 | CACHECTIME = 1136 196 | CACHEPKGPATH = 1137 197 | CACHEPKGSIZE = 1138 198 | CACHEPKGMTIME = 1139 199 | FILECOLORS = 1140 200 | FILECLASS = 1141 201 | CLASSDICT = 1142 202 | FILEDEPENDSX = 1143 203 | FILEDEPENDSN = 1144 204 | DEPENDSDICT = 1145 205 | SOURCEPKGID = 1146 206 | FILECONTEXTS = 1147 207 | FSCONTEXTS = 1148 208 | RECONTEXTS = 1149 209 | POLICIES = 1150 210 | PRETRANS = 1151 211 | POSTTRANS = 1152 212 | PRETRANSPROG = 1153 213 | POSTTRANSPROG = 1154 214 | DISTTAG = 1155 215 | SUGGESTSNAME = 1156 216 | SUGGESTSVERSION = 1157 217 | SUGGESTSFLAGS = 1158 218 | ENHANCESNAME = 1159 219 | ENHANCESVERSION = 1160 220 | ENHANCESFLAGS = 1161 221 | PRIORITY = 1162 222 | CVSID = 1163 223 | BLINKPKGID = 1164 224 | BLINKHDRID = 1165 225 | BLINKNEVRA = 1166 226 | FLINKPKGID = 1167 227 | FLINKHDRID = 1168 228 | FLINKNEVRA = 1169 229 | PACKAGEORIGIN = 1170 230 | TRIGGERPREIN = 1171 231 | BUILDSUGGESTS = 1172 232 | BUILDENHANCES = 1173 233 | SCRIPTSTATES = 1174 234 | SCRIPTMETRICS = 1175 235 | BUILDCPUCLOCK = 1176 236 | FILEDIGESTALGOS = 1177 237 | VARIANTS = 1178 238 | XMAJOR = 1179 239 | XMINOR = 1180 240 | REPOTAG = 1181 241 | KEYWORDS = 1182 242 | BUILDPLATFORMS = 1183 243 | PACKAGECOLOR = 1184 244 | PACKAGEPREFCOLOR = 1185 245 | XATTRSDICT = 1186 246 | FILEXATTRSX = 1187 247 | DEPATTRSDICT = 1188 248 | CONFLICTATTRSX = 1189 249 | OBSOLETEATTRSX = 1190 250 | PROVIDEATTRSX = 1191 251 | REQUIREATTRSX = 1192 252 | BUILDPROVIDES = 1193 253 | BUILDOBSOLETES = 1194 254 | DBINSTANCE = 1195 255 | NVRA = 1196 256 | FILENAMES = 5000 257 | FILEPROVIDE = 5001 258 | FILEREQUIRE = 5002 259 | FSNAMES = 5003 260 | FSSIZES = 5004 261 | TRIGGERCONDS = 5005 262 | TRIGGERTYPE = 5006 263 | ORIGFILENAMES = 5007 264 | LONGFILESIZES = 5008 265 | LONGSIZE = 5009 266 | FILECAPS = 5010 267 | FILEDIGESTALGO = 5011 268 | BUGURL = 5012 269 | EVR = 5013 270 | NVR = 5014 271 | NEVR = 5015 272 | NEVRA = 5016 273 | HEADERCOLOR = 5017 274 | VERBOSE = 5018 275 | EPOCHNUM = 5019 276 | ENCODING = 5062 277 | 278 | TAG_MAP = Hash[constants.collect { |c| [const_get(c), c] }] 279 | 280 | attr_accessor :tag, :type, :value 281 | 282 | def initialize(tag_number, type_number) 283 | @tag = self.class::TAG_MAP[tag_number] || tag_number 284 | @type = type_number 285 | end 286 | 287 | def parse(data, offset, count) 288 | @value = Type.parse(data, @type, offset, count) 289 | nil 290 | end 291 | 292 | def inspect 293 | format("<%s#%s> %s/%d value=%s>", self.class.name, self.object_id, @tag, @type, @value.inspect) 294 | end 295 | end # module ArrPM::V2::Tag 296 | -------------------------------------------------------------------------------- /lib/arr-pm/v2/type.rb: -------------------------------------------------------------------------------- 1 | require "arr-pm/namespace" 2 | 3 | module ArrPM::V2::Type 4 | BINARY = 0 5 | SOURCE = 1 6 | 7 | module_function 8 | 9 | # Is a given rpm type value valid? 10 | # 11 | # The only valid types are BINARY (0) or SOURCE (1) 12 | def valid?(value) 13 | return (value == BINARY || value == SOURCE) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/arr-pm/v2/header_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "flores/random" 4 | 5 | require "arr-pm/v2/header" 6 | require "arr-pm/v2/type" 7 | require "arr-pm/v2/architecture" 8 | require "json" 9 | 10 | describe ArrPM::V2::Header do 11 | context "with a known good rpm" do 12 | let(:path) { File.join(File.dirname(__FILE__), "../../fixtures/example-1.0-1.x86_64.rpm") } 13 | let(:file) { File.new(path) } 14 | 15 | before do 16 | lead = ArrPM::V2::Lead.new 17 | lead.load(file) 18 | 19 | # Throw away the signature if we have one 20 | described_class.new.load(file) if lead.signature? 21 | 22 | subject.load(file) 23 | end 24 | 25 | expectations = JSON.parse(File.read(File.join(File.dirname(__FILE__), "../../fixtures/example.json"))) 26 | 27 | expectations.each do |name, expected_value| 28 | it "should have expected value for the #{name} tag" do 29 | tag = subject.tags.find { |t| t.tag.to_s == name } 30 | expect(tag.value).to be == expected_value 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/arr-pm/v2/lead_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "flores/random" 4 | 5 | require "arr-pm/v2/lead" 6 | require "arr-pm/v2/type" 7 | require "arr-pm/v2/architecture" 8 | 9 | describe ArrPM::V2::Lead do 10 | let(:major) { Flores::Random.integer(0..255) } 11 | let(:minor) { Flores::Random.integer(0..255) } 12 | let(:magic) { described_class::MAGIC } 13 | let(:type) { ArrPM::V2::Type::BINARY } 14 | let(:architecture) { ArrPM::V2::Architecture::I386 } 15 | let(:os) { 0 } 16 | let(:os_bytes) { [os].pack("n").unpack("C2") } 17 | let(:signature_type) { 0 } 18 | let(:signature_type_bytes) { [signature_type].pack("n").unpack("C2") } 19 | let(:longname) { "test-1.0-1" } 20 | let(:longnamebytes) { longname.bytes + (66-longname.bytesize).times.collect { 0 } } 21 | let(:leadbytes) { magic + [major, minor] + [0, type, 0, architecture] + longnamebytes + os_bytes + signature_type_bytes + 16.times.collect { 0 } } 22 | let(:lead) { leadbytes.pack("C*") } 23 | 24 | describe ".parse_magic" do 25 | context "when given an invalid magic value" do 26 | # Generate random bytes for the magic value, should be bad. 27 | let(:magic) { Flores::Random.iterations(0..10).collect { Flores::Random.integer(0..255) } } 28 | 29 | it "should fail" do 30 | expect { described_class.parse_magic(leadbytes) }.to raise_error(ArrPM::V2::Error::InvalidMagicValue) 31 | end 32 | end 33 | 34 | context "when given a valid magic value" do 35 | it "should succeed" do 36 | expect { described_class.parse_magic(leadbytes) }.not_to raise_error 37 | end 38 | end 39 | end 40 | 41 | describe ".parse_version" do 42 | context "when given an invalid version value" do 43 | let(:data) { magic + [major, minor] } 44 | 45 | it "should return an array of two values " do 46 | expect(described_class.parse_version(leadbytes)).to be == [major, minor] 47 | end 48 | end 49 | end 50 | 51 | describe ".parse_type" do 52 | context "when given an invalid type" do 53 | let(:type) { Flores::Random.integer(2..1000) } 54 | it "should fail" do 55 | expect { described_class.parse_type(leadbytes) }.to raise_error(ArrPM::V2::Error::InvalidType) 56 | end 57 | end 58 | context "with a valid type" do 59 | it "should return the type" do 60 | expect(described_class.parse_type(leadbytes)).to be == type 61 | end 62 | end 63 | end 64 | 65 | describe ".parse_name" do 66 | context "with a valid name" do 67 | it "should return the name" do 68 | expect(described_class.parse_name(leadbytes)).to be == longname 69 | end 70 | end 71 | end 72 | 73 | describe ".parse_signature_type" do 74 | it "should return the signature type" do 75 | expect(described_class.parse_signature_type(leadbytes)).to be == signature_type 76 | end 77 | end 78 | 79 | describe ".parse_reserved" do 80 | it "should return exactly 16 bytes" do 81 | expect(described_class.parse_reserved(leadbytes).count).to be == 16 82 | end 83 | end 84 | 85 | describe "#parse" do 86 | before do 87 | subject.parse(lead) 88 | end 89 | 90 | it "should have a correct parsed values" do 91 | expect(subject.name).to be == longname 92 | expect(subject.major).to be == major 93 | expect(subject.minor).to be == minor 94 | expect(subject.type).to be == type 95 | expect(subject.architecture).to be == architecture 96 | end 97 | end 98 | 99 | describe "#dump" do 100 | before do 101 | subject.parse(lead) 102 | end 103 | 104 | let(:blob) { subject.serialize } 105 | 106 | it "should parse successfully" do 107 | subject.parse(blob) 108 | end 109 | end 110 | 111 | context "with a known good rpm" do 112 | let(:path) { File.join(File.dirname(__FILE__), "../../fixtures/example-1.0-1.x86_64.rpm") } 113 | 114 | before do 115 | subject.load(File.new(path)) 116 | end 117 | 118 | it "should have expected values" do 119 | expect(subject.name).to be == "example-1.0-1" 120 | expect(subject.major).to be == 3 121 | expect(subject.minor).to be == 0 122 | expect(subject.architecture).to be == architecture 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/fixtures/example-1.0-1.x86_64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordansissel/ruby-arr-pm/441764ab706cbf61d55d4ba5b0b04959b635c360/spec/fixtures/example-1.0-1.x86_64.rpm -------------------------------------------------------------------------------- /spec/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "NAME": "example", 3 | "VERSION": "1.0", 4 | "RELEASE": "1", 5 | "SUMMARY": "no description given", 6 | "DESCRIPTION": "no description given", 7 | "BUILDTIME": [ 8 | 1466326707 9 | ], 10 | "BUILDHOST": "localhost", 11 | "SIZE": [ 12 | 0 13 | ], 14 | "VENDOR": "none", 15 | "LICENSE": "unknown", 16 | "PACKAGER": "", 17 | "GROUP": "default", 18 | "URL": "http://example.com/no-uri-given", 19 | "OS": "linux", 20 | "ARCH": "x86_64", 21 | "SOURCERPM": "example-1.0-1.src.rpm", 22 | "PROVIDENAME": [ 23 | "example", 24 | "example(x86-64)" 25 | ], 26 | "REQUIREFLAGS": [ 27 | 16777226, 28 | 16777226 29 | ], 30 | "REQUIRENAME": [ 31 | "rpmlib(CompressedFileNames)", 32 | "rpmlib(PayloadFilesHavePrefix)" 33 | ], 34 | "REQUIREVERSION": [ 35 | "3.0.4-1", 36 | "4.0-1" 37 | ], 38 | "RPMVERSION": "4.13.0-rc1", 39 | "PREFIXES": [ 40 | "/" 41 | ], 42 | "PROVIDEFLAGS": [ 43 | 8, 44 | 8 45 | ], 46 | "PROVIDEVERSION": [ 47 | "1.0-1", 48 | "1.0-1" 49 | ], 50 | "PAYLOADFORMAT": "cpio", 51 | "PAYLOADCOMPRESSOR": "gzip", 52 | "PAYLOADFLAGS": "9", 53 | "PLATFORM": "x86_64-redhat-linux-gnu", 54 | "ENCODING": "utf-8" 55 | } 56 | -------------------------------------------------------------------------------- /spec/fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordansissel/ruby-arr-pm/441764ab706cbf61d55d4ba5b0b04959b635c360/spec/fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm -------------------------------------------------------------------------------- /spec/rpm/file_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "arr-pm/file" 4 | require "stud/temporary" 5 | require "insist" 6 | 7 | describe ::RPM::File do 8 | subject { described_class.new(path) } 9 | 10 | context "with a known good rpm" do 11 | let(:path) { File.join(File.dirname(__FILE__), "../fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm") } 12 | 13 | context "#files" do 14 | let(:files) { [ 15 | "/usr/lib/systemd/system/pagure_mirror.service", 16 | "/usr/share/licenses/pagure-mirror", 17 | "/usr/share/licenses/pagure-mirror/LICENSE" 18 | ]} 19 | 20 | it "should have the correct list of files" do 21 | expect(subject.files).to eq(files) 22 | end 23 | end 24 | end 25 | 26 | context "#extract" do 27 | # This RPM should be correctly built, but we will modify the tags at runtime to force an error. 28 | let(:path) { File.join(File.dirname(__FILE__), "../fixtures/example-1.0-1.x86_64.rpm") } 29 | let(:dir) { dir = Stud::Temporary.directory } 30 | 31 | after do 32 | FileUtils.rm_rf(dir) 33 | end 34 | 35 | context "with an invalid payloadcompressor" do 36 | before do 37 | subject.tags[:payloadcompressor] = "some invalid | string" 38 | end 39 | 40 | it "should raise an error" do 41 | insist { subject.extract(dir) }.raises(RuntimeError) 42 | end 43 | end 44 | 45 | [ "gzip", "bzip2", "xz", "lzma", "zstd" ].each do |name| 46 | context "with a '#{name}' payloadcompressor" do 47 | before do 48 | subject.tags[:payloadcompressor] = name 49 | end 50 | 51 | it "should succeed" do 52 | reject { subject.extract(dir) }.raises(RuntimeError) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | --------------------------------------------------------------------------------