depot-linux 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/README.md +56 -0
- data/Rakefile +10 -0
- data/bin/depot +8 -0
- data/bin/depot-gui +14 -0
- data/bin/setup-rubyqt6 +72 -0
- data/fixtures/assets/download.png +0 -0
- data/fixtures/flatpakrefs/org.qbittorrent.qBittorrent.flatpakref +10 -0
- data/fixtures/rpms/Modrinth App-0.13.14-1.x86_64.rpm +0 -0
- data/lib/depot/app_customizer.rb +152 -0
- data/lib/depot/assets.rb +11 -0
- data/lib/depot/backends/app_image.rb +210 -0
- data/lib/depot/backends/archive.rb +263 -0
- data/lib/depot/backends/deb.rb +265 -0
- data/lib/depot/backends/flatpak_ref.rb +123 -0
- data/lib/depot/backends/rpm.rb +280 -0
- data/lib/depot/backends/support.rb +39 -0
- data/lib/depot/cli.rb +344 -0
- data/lib/depot/desktop_entry.rb +37 -0
- data/lib/depot/doctor.rb +59 -0
- data/lib/depot/gui/app.rb +23 -0
- data/lib/depot/gui/drop_panel.rb +80 -0
- data/lib/depot/gui/main_window.rb +1196 -0
- data/lib/depot/inspection.rb +52 -0
- data/lib/depot/inspector.rb +387 -0
- data/lib/depot/installer.rb +54 -0
- data/lib/depot/manifest_store.rb +53 -0
- data/lib/depot/packages/archive.rb +188 -0
- data/lib/depot/packages/deb.rb +262 -0
- data/lib/depot/packages/flatpak_ref.rb +90 -0
- data/lib/depot/packages/rpm.rb +301 -0
- data/lib/depot/paths.rb +57 -0
- data/lib/depot/result.rb +13 -0
- data/lib/depot/sandbox.rb +285 -0
- data/lib/depot/settings.rb +43 -0
- data/lib/depot/source_resolver.rb +36 -0
- data/lib/depot/uninstaller.rb +90 -0
- data/lib/depot/update_downloader.rb +136 -0
- data/lib/depot/updater.rb +230 -0
- data/lib/depot/util.rb +33 -0
- data/lib/depot/version.rb +5 -0
- data/lib/depot.rb +21 -0
- metadata +139 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Depot
|
|
7
|
+
class ArchivePackage
|
|
8
|
+
DESKTOP_PATH = %r{\A(?:\./)?(.+/)?[^/]+\.desktop\z}
|
|
9
|
+
HICOLOR_ICON_PATH = %r{/icons/hicolor/([^/]+)/apps/([^/]+)\.(png|svg|xpm)\z}i
|
|
10
|
+
IMAGE_EXT = /\.(png|svg|xpm)\z/i
|
|
11
|
+
SCRIPT_EXT = /\.(sh|bash|run|pl|py|rb)\z/i
|
|
12
|
+
SOURCE_MARKERS = %w[configure Makefile CMakeLists.txt meson.build setup.py package.json Cargo.toml go.mod].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :path
|
|
15
|
+
|
|
16
|
+
def initialize(path)
|
|
17
|
+
@path = path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def valid?
|
|
21
|
+
tar_name? && !members.empty?
|
|
22
|
+
rescue FormatError
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def format
|
|
27
|
+
case File.basename(path)
|
|
28
|
+
when /\.t(?:ar\.)?gz\z/i, /\.tgz\z/i then "tar.gz"
|
|
29
|
+
when /\.tar\.xz\z/i, /\.txz\z/i then "tar.xz"
|
|
30
|
+
when /\.tar\.zst\z/i, /\.tzst\z/i then "tar.zst"
|
|
31
|
+
else "archive"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def display_name
|
|
36
|
+
root = common_root
|
|
37
|
+
return titleize(root) if root && root != "."
|
|
38
|
+
|
|
39
|
+
titleize(File.basename(path).sub(/\.(tar\.gz|tgz|tar\.xz|txz|tar\.zst|tzst)\z/i, ""))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def members
|
|
43
|
+
@members ||= list_archive
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def clean_members
|
|
47
|
+
members.map { |entry| clean_entry(entry) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def desktop_entries
|
|
51
|
+
clean_members.select { |entry| entry.end_with?(".desktop") }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def primary_desktop_entry
|
|
55
|
+
desktop_entries.min_by { |entry| desktop_score(entry) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def image_entries
|
|
59
|
+
clean_members.select { |entry| entry.match?(IMAGE_EXT) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def icon_entries
|
|
63
|
+
clean_members.select { |entry| entry.match?(HICOLOR_ICON_PATH) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def executable_candidates
|
|
67
|
+
clean_members.reject { |entry| entry.end_with?("/") }
|
|
68
|
+
.select { |entry| executable_name?(entry) }
|
|
69
|
+
.first(24)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def script_entries
|
|
73
|
+
clean_members.reject { |entry| entry.end_with?("/") }
|
|
74
|
+
.select { |entry| File.basename(entry).match?(SCRIPT_EXT) || File.basename(entry).match?(/\Ainstall/i) }
|
|
75
|
+
.first(24)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def source_markers
|
|
79
|
+
clean_members.map { |entry| File.basename(entry) }
|
|
80
|
+
.select { |name| SOURCE_MARKERS.include?(name) }
|
|
81
|
+
.uniq
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def common_root
|
|
85
|
+
roots = clean_members.reject(&:empty?).map { |entry| entry.split("/", 2).first }.uniq
|
|
86
|
+
roots.length == 1 ? roots.first : nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def read_entry(entry)
|
|
90
|
+
wanted = clean_entry(entry)
|
|
91
|
+
candidate = members.find { |member| clean_entry(member) == wanted }
|
|
92
|
+
raise FormatError, "Could not find #{entry} in archive" unless candidate
|
|
93
|
+
|
|
94
|
+
stdout, stderr, status = Open3.capture3("tar", *tar_options, "-xOf", path, candidate)
|
|
95
|
+
raise FormatError, "Could not read #{entry}: #{stderr.empty? ? stdout : stderr}" unless status.success?
|
|
96
|
+
|
|
97
|
+
stdout
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extract_to(destination)
|
|
101
|
+
FileUtils.mkdir_p(destination)
|
|
102
|
+
assert_safe_entries!
|
|
103
|
+
stdout, stderr, status = Open3.capture3("tar", *tar_options, "-xf", path, "-C", destination)
|
|
104
|
+
raise FormatError, "Could not extract archive: #{stderr.empty? ? stdout : stderr}" unless status.success?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class FormatError < StandardError; end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def tar_name?
|
|
112
|
+
File.basename(path).match?(/\.(tar\.gz|tgz|tar\.xz|txz|tar\.zst|tzst)\z/i)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def list_archive
|
|
116
|
+
stdout, stderr, status = Open3.capture3("tar", *tar_options, "-tf", path)
|
|
117
|
+
raise FormatError, "Could not list archive: #{stderr.empty? ? stdout : stderr}" unless status.success?
|
|
118
|
+
|
|
119
|
+
stdout.lines.map(&:chomp).reject(&:empty?)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def tar_options
|
|
123
|
+
case File.basename(path)
|
|
124
|
+
when /\.(tar\.gz|tgz)\z/i then ["-z"]
|
|
125
|
+
when /\.(tar\.xz|txz)\z/i then ["-J"]
|
|
126
|
+
when /\.(tar\.zst|tzst)\z/i then ["--zstd"]
|
|
127
|
+
else []
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def assert_safe_entries!
|
|
132
|
+
unsafe = members.find do |entry|
|
|
133
|
+
clean = clean_entry(entry)
|
|
134
|
+
entry.start_with?("/") || clean.split("/").include?("..")
|
|
135
|
+
end
|
|
136
|
+
raise FormatError, "Unsafe path in archive: #{unsafe}" if unsafe
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def clean_entry(entry)
|
|
140
|
+
entry.to_s.sub(%r{\A\./}, "")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def executable_name?(entry)
|
|
144
|
+
base = File.basename(entry)
|
|
145
|
+
return false if SOURCE_MARKERS.include?(base)
|
|
146
|
+
return false if base.match?(/\A(install|setup|configure)\b/i)
|
|
147
|
+
return true if base == "AppRun"
|
|
148
|
+
return true if entry.include?("/bin/") && !base.include?(".")
|
|
149
|
+
return true if base.match?(SCRIPT_EXT)
|
|
150
|
+
|
|
151
|
+
!base.include?(".") && !entry.include?("/share/") && !entry.include?("/doc/")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def desktop_score(entry)
|
|
155
|
+
metadata = parse_desktop_entry(read_entry(entry))
|
|
156
|
+
[
|
|
157
|
+
metadata["NoDisplay"].to_s.downcase == "true" ? 1 : 0,
|
|
158
|
+
metadata["Hidden"].to_s.downcase == "true" ? 1 : 0,
|
|
159
|
+
metadata["Type"] == "Application" ? 0 : 1,
|
|
160
|
+
clean_entry(entry).match?(%r{/share/applications/[^/]+\.desktop\z}) ? 0 : 1,
|
|
161
|
+
metadata["Name"].to_s.downcase.include?("url handler") ? 1 : 0,
|
|
162
|
+
clean_entry(entry).length
|
|
163
|
+
]
|
|
164
|
+
rescue FormatError
|
|
165
|
+
[9, clean_entry(entry).length]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parse_desktop_entry(contents)
|
|
169
|
+
metadata = {}
|
|
170
|
+
in_desktop_entry = false
|
|
171
|
+
contents.each_line(chomp: true) do |line|
|
|
172
|
+
if line.start_with?("[")
|
|
173
|
+
in_desktop_entry = line.strip == "[Desktop Entry]"
|
|
174
|
+
next
|
|
175
|
+
end
|
|
176
|
+
next unless in_desktop_entry
|
|
177
|
+
|
|
178
|
+
key, value = line.split("=", 2)
|
|
179
|
+
metadata[key] = value if value && !metadata.key?(key)
|
|
180
|
+
end
|
|
181
|
+
metadata
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def titleize(value)
|
|
185
|
+
value.to_s.gsub(/[_-]+/, " ").split.map(&:capitalize).join(" ")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
|
|
8
|
+
module Depot
|
|
9
|
+
class DebPackage
|
|
10
|
+
SCRIPT_NAMES = %w[preinst postinst prerm postrm config].freeze
|
|
11
|
+
DESKTOP_PATH = %r{\A(?:\./)?usr/share/applications/[^/]+\.desktop\z}
|
|
12
|
+
ICON_PATH = %r{/icons/hicolor/([^/]+)/apps/([^/]+)\.(png|svg|xpm)\z}i
|
|
13
|
+
|
|
14
|
+
attr_reader :path
|
|
15
|
+
|
|
16
|
+
def initialize(path)
|
|
17
|
+
@path = path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def valid?
|
|
21
|
+
debian_binary && control_archive_name && data_archive_name
|
|
22
|
+
rescue FormatError
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def debian_binary
|
|
27
|
+
member("debian-binary")&.strip
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def control_fields
|
|
31
|
+
@control_fields ||= parse_control(read_control_file("control").to_s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def control_members
|
|
35
|
+
@control_members ||= list_tar(control_archive_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def data_members
|
|
39
|
+
@data_members ||= list_tar(data_archive_name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def maintainer_scripts
|
|
43
|
+
control_members.map { |entry| clean_entry(entry) }
|
|
44
|
+
.select { |entry| SCRIPT_NAMES.include?(File.basename(entry)) }
|
|
45
|
+
.uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def desktop_entries
|
|
49
|
+
data_members.map { |entry| clean_entry(entry) }
|
|
50
|
+
.select { |entry| entry.end_with?(".desktop") }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def primary_desktop_entry
|
|
54
|
+
desktop_entries.min_by { |entry| desktop_score(entry) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def icon_entries
|
|
58
|
+
data_members.map { |entry| clean_entry(entry) }
|
|
59
|
+
.select { |entry| entry.match?(ICON_PATH) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def image_entries
|
|
63
|
+
data_members.map { |entry| clean_entry(entry) }
|
|
64
|
+
.select { |entry| entry.match?(/\.(png|svg|xpm)\z/i) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def executable_entries
|
|
68
|
+
data_members.map { |entry| clean_entry(entry) }
|
|
69
|
+
.select { |entry| entry.start_with?("usr/bin/", "usr/local/bin/", "opt/") }
|
|
70
|
+
.reject { |entry| entry.end_with?("/") }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def control_archive_name
|
|
74
|
+
@control_archive_name ||= archive_name("control.tar")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def data_archive_name
|
|
78
|
+
@data_archive_name ||= archive_name("data.tar")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def ar_members
|
|
82
|
+
@ar_members ||= parse_ar.keys
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read_data_entry(entry)
|
|
86
|
+
read_tar_entry(data_archive_name, entry)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def extract_data_to(destination)
|
|
90
|
+
FileUtils.mkdir_p(destination)
|
|
91
|
+
assert_safe_entries!(data_members)
|
|
92
|
+
with_member_file(data_archive_name) do |archive|
|
|
93
|
+
stdout, stderr, status = Open3.capture3(
|
|
94
|
+
"tar",
|
|
95
|
+
*tar_options(data_archive_name),
|
|
96
|
+
"-xf",
|
|
97
|
+
archive,
|
|
98
|
+
"-C",
|
|
99
|
+
destination
|
|
100
|
+
)
|
|
101
|
+
raise FormatError, "Could not extract data archive: #{stderr.empty? ? stdout : stderr}" unless status.success?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
class FormatError < StandardError; end
|
|
108
|
+
|
|
109
|
+
def member(name)
|
|
110
|
+
parse_ar[normalize_ar_name(name)]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def archive_name(prefix)
|
|
114
|
+
ar_members.find { |name| name.start_with?(prefix) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def read_control_file(name)
|
|
118
|
+
read_tar_entry(control_archive_name, name)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def read_tar_entry(archive_name, entry)
|
|
122
|
+
wanted = clean_entry(entry)
|
|
123
|
+
candidate = list_tar(archive_name).find { |member| clean_entry(member) == wanted }
|
|
124
|
+
raise FormatError, "Could not find #{entry} in #{archive_name}" unless candidate
|
|
125
|
+
|
|
126
|
+
with_member_file(archive_name) do |archive|
|
|
127
|
+
stdout, stderr, status = Open3.capture3(
|
|
128
|
+
"tar",
|
|
129
|
+
*tar_options(archive_name),
|
|
130
|
+
"-xOf",
|
|
131
|
+
archive,
|
|
132
|
+
candidate
|
|
133
|
+
)
|
|
134
|
+
raise FormatError, "Could not read #{entry}: #{stderr.empty? ? stdout : stderr}" unless status.success?
|
|
135
|
+
|
|
136
|
+
stdout
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def list_tar(archive_name)
|
|
141
|
+
with_member_file(archive_name) do |archive|
|
|
142
|
+
stdout, stderr, status = Open3.capture3("tar", *tar_options(archive_name), "-tf", archive)
|
|
143
|
+
raise FormatError, "Could not list #{archive_name}: #{stderr.empty? ? stdout : stderr}" unless status.success?
|
|
144
|
+
|
|
145
|
+
stdout.lines.map(&:chomp).reject(&:empty?)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def with_member_file(name)
|
|
150
|
+
data = member(name)
|
|
151
|
+
raise FormatError, "Missing #{name}" unless data
|
|
152
|
+
|
|
153
|
+
file = Tempfile.new(["depot-deb-", File.extname(name)])
|
|
154
|
+
file.binmode
|
|
155
|
+
file.write(data)
|
|
156
|
+
file.close
|
|
157
|
+
yield file.path
|
|
158
|
+
ensure
|
|
159
|
+
file&.unlink
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def tar_options(name)
|
|
163
|
+
case name
|
|
164
|
+
when /\.tar\.xz\z/ then ["-J"]
|
|
165
|
+
when /\.tar\.gz\z/ then ["-z"]
|
|
166
|
+
when /\.tar\.bz2\z/ then ["-j"]
|
|
167
|
+
when /\.tar\.zst\z/ then ["--zstd"]
|
|
168
|
+
else []
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def parse_control(text)
|
|
173
|
+
fields = {}
|
|
174
|
+
current = nil
|
|
175
|
+
text.each_line(chomp: true) do |line|
|
|
176
|
+
if line.start_with?(" ", "\t")
|
|
177
|
+
fields[current] = [fields[current], line.sub(/\A[ \t]/, "")].compact.join("\n") if current
|
|
178
|
+
next
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
key, value = line.split(":", 2)
|
|
182
|
+
next unless value
|
|
183
|
+
|
|
184
|
+
current = key
|
|
185
|
+
fields[key] = value.strip
|
|
186
|
+
end
|
|
187
|
+
fields
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def assert_safe_entries!(entries)
|
|
191
|
+
unsafe = entries.find do |entry|
|
|
192
|
+
clean = clean_entry(entry)
|
|
193
|
+
entry.start_with?("/") || clean.split("/").include?("..")
|
|
194
|
+
end
|
|
195
|
+
raise FormatError, "Unsafe path in data archive: #{unsafe}" if unsafe
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def clean_entry(entry)
|
|
199
|
+
entry.to_s.sub(%r{\A\./}, "")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def desktop_score(entry)
|
|
203
|
+
metadata = parse_desktop_entry(read_data_entry(entry))
|
|
204
|
+
[
|
|
205
|
+
metadata["NoDisplay"].to_s.downcase == "true" ? 1 : 0,
|
|
206
|
+
metadata["Hidden"].to_s.downcase == "true" ? 1 : 0,
|
|
207
|
+
metadata["Type"] == "Application" ? 0 : 1,
|
|
208
|
+
clean_entry(entry).match?(DESKTOP_PATH) ? 0 : 1,
|
|
209
|
+
metadata["Name"].to_s.downcase.include?("url handler") ? 1 : 0,
|
|
210
|
+
File.basename(entry).include?("-url-handler") ? 1 : 0,
|
|
211
|
+
clean_entry(entry).length
|
|
212
|
+
]
|
|
213
|
+
rescue FormatError
|
|
214
|
+
[9, clean_entry(entry).length]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_desktop_entry(contents)
|
|
218
|
+
metadata = {}
|
|
219
|
+
in_desktop_entry = false
|
|
220
|
+
contents.each_line(chomp: true) do |line|
|
|
221
|
+
if line.start_with?("[")
|
|
222
|
+
in_desktop_entry = line.strip == "[Desktop Entry]"
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
next unless in_desktop_entry
|
|
226
|
+
|
|
227
|
+
key, value = line.split("=", 2)
|
|
228
|
+
metadata[key] = value if value && !metadata.key?(key)
|
|
229
|
+
end
|
|
230
|
+
metadata
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def parse_ar
|
|
234
|
+
@parse_ar ||= begin
|
|
235
|
+
File.open(path, "rb") do |file|
|
|
236
|
+
raise FormatError, "Not an ar archive" unless file.read(8) == "!<arch>\n"
|
|
237
|
+
|
|
238
|
+
members = {}
|
|
239
|
+
until file.eof?
|
|
240
|
+
header = file.read(60)
|
|
241
|
+
break if header.nil? || header.empty?
|
|
242
|
+
raise FormatError, "Truncated ar header" unless header.bytesize == 60
|
|
243
|
+
raise FormatError, "Invalid ar member header" unless header.byteslice(58, 2) == "`\n"
|
|
244
|
+
|
|
245
|
+
name = normalize_ar_name(header.byteslice(0, 16))
|
|
246
|
+
size = header.byteslice(48, 10).to_s.strip.to_i
|
|
247
|
+
data = file.read(size)
|
|
248
|
+
raise FormatError, "Truncated ar member #{name}" unless data && data.bytesize == size
|
|
249
|
+
|
|
250
|
+
file.read(1) if size.odd?
|
|
251
|
+
members[name] = data
|
|
252
|
+
end
|
|
253
|
+
members
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def normalize_ar_name(name)
|
|
259
|
+
name.to_s.strip.sub(%r{/\z}, "")
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Depot
|
|
4
|
+
class FlatpakRef
|
|
5
|
+
GROUP = "Flatpak Ref"
|
|
6
|
+
BOOLEAN_KEYS = %w[IsRuntime].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :path
|
|
9
|
+
|
|
10
|
+
def initialize(path)
|
|
11
|
+
@path = path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?
|
|
15
|
+
fields.fetch("Name", "").to_s != "" && fields.fetch("Url", "").to_s != ""
|
|
16
|
+
rescue FormatError
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fields
|
|
21
|
+
@fields ||= parse
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def name
|
|
25
|
+
fields["Name"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def branch
|
|
29
|
+
fields["Branch"] || "master"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def title
|
|
33
|
+
fields["Title"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def display_name
|
|
37
|
+
title.to_s.sub(/\s+from\s+\S+\z/i, "").then { |value| value.empty? ? name : value }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def runtime?
|
|
41
|
+
fields["IsRuntime"] == true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def remote_name
|
|
45
|
+
fields["SuggestRemoteName"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def url
|
|
49
|
+
fields["Url"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def runtime_repo
|
|
53
|
+
fields["RuntimeRepo"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def gpg_key?
|
|
57
|
+
fields["GPGKey"].to_s.strip != ""
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class FormatError < StandardError; end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def parse
|
|
65
|
+
current_group = nil
|
|
66
|
+
parsed = {}
|
|
67
|
+
File.foreach(path, chomp: true) do |line|
|
|
68
|
+
stripped = line.strip
|
|
69
|
+
next if stripped.empty? || stripped.start_with?("#", ";")
|
|
70
|
+
|
|
71
|
+
if stripped.start_with?("[") && stripped.end_with?("]")
|
|
72
|
+
current_group = stripped[1...-1]
|
|
73
|
+
next
|
|
74
|
+
end
|
|
75
|
+
next unless current_group == GROUP
|
|
76
|
+
|
|
77
|
+
key, value = stripped.split("=", 2)
|
|
78
|
+
next unless key && value
|
|
79
|
+
|
|
80
|
+
parsed[key] = BOOLEAN_KEYS.include?(key) ? value.downcase == "true" : value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
raise FormatError, "Missing [#{GROUP}] group" if parsed.empty?
|
|
84
|
+
|
|
85
|
+
parsed
|
|
86
|
+
rescue SystemCallError => e
|
|
87
|
+
raise FormatError, e.message
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|