pray-cli 1.9.1 → 1.10.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/bin/pray +0 -1
- data/lib/pray/archive.rb +18 -16
- data/lib/pray/archive_unpack.rb +68 -0
- data/lib/pray/cli/commands/workflow.rb +1 -1
- data/lib/pray/cli/helpers.rb +2 -1
- data/lib/pray/compose_dest.rb +59 -0
- data/lib/pray/format_serialize.rb +6 -1
- data/lib/pray/http_body.rb +34 -0
- data/lib/pray/lockfile.rb +32 -6
- data/lib/pray/lockfile_serialize.rb +15 -0
- data/lib/pray/manifest.rb +26 -9
- data/lib/pray/manifest_json.rb +4 -4
- data/lib/pray/manifest_parser.rb +1 -0
- data/lib/pray/manifest_parser_blocks.rb +5 -2
- data/lib/pray/manifest_parser_helpers.rb +34 -5
- data/lib/pray/materialize.rb +5 -3
- data/lib/pray/package_spec.rb +19 -0
- data/lib/pray/path_safety.rb +43 -0
- data/lib/pray/plan.rb +5 -11
- data/lib/pray/registry.rb +33 -70
- data/lib/pray/registry_install.rb +30 -0
- data/lib/pray/registry_integrity.rb +63 -0
- data/lib/pray/render.rb +43 -65
- data/lib/pray/render_content.rb +36 -0
- data/lib/pray/render_dest.rb +215 -0
- data/lib/pray/render_layout.rb +21 -0
- data/lib/pray/render_patch.rb +107 -0
- data/lib/pray/resolve.rb +11 -54
- data/lib/pray/resolve_exports.rb +79 -0
- data/lib/pray/resolve_source.rb +28 -0
- data/lib/pray/resource_limits.rb +14 -0
- data/lib/pray/serve.rb +46 -4
- data/lib/pray/session.rb +36 -4
- data/lib/pray/tar_validation.rb +157 -0
- data/lib/pray/verify.rb +2 -0
- data/lib/pray/verify_provisioned.rb +55 -0
- data/lib/pray/version.rb +2 -2
- data/lib/pray.rb +12 -0
- metadata +15 -1
data/lib/pray/session.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require_relative "trust"
|
|
5
6
|
|
|
6
7
|
module Pray
|
|
7
8
|
SessionFile = Struct.new(
|
|
@@ -12,13 +13,14 @@ module Pray
|
|
|
12
13
|
module Session
|
|
13
14
|
module_function
|
|
14
15
|
|
|
15
|
-
def session_file_path(
|
|
16
|
-
File.join(
|
|
16
|
+
def session_file_path(_root)
|
|
17
|
+
File.join(Trust.trust_home, "session.json")
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def persist(root, session)
|
|
20
21
|
path = session_file_path(root)
|
|
21
22
|
FileUtils.mkdir_p(File.dirname(path))
|
|
23
|
+
migrate_legacy_session(root, path)
|
|
22
24
|
sessions = load_sessions(path)
|
|
23
25
|
existing = sessions.find { |entry| entry.server_url == session.server_url }
|
|
24
26
|
if existing
|
|
@@ -34,12 +36,14 @@ module Pray
|
|
|
34
36
|
else
|
|
35
37
|
{"sessions" => sessions.map { |entry| session_to_hash(entry) }}
|
|
36
38
|
end
|
|
37
|
-
|
|
39
|
+
write_document(path, document)
|
|
38
40
|
session
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
def load_latest(root)
|
|
42
|
-
|
|
44
|
+
path = session_file_path(root)
|
|
45
|
+
migrate_legacy_session(root, path)
|
|
46
|
+
sessions = load_sessions(path)
|
|
43
47
|
sessions.reverse.find { |session| !session.email.to_s.strip.empty? }
|
|
44
48
|
end
|
|
45
49
|
|
|
@@ -86,5 +90,33 @@ module Pray
|
|
|
86
90
|
signer_fingerprint: entry["signer_fingerprint"]
|
|
87
91
|
)
|
|
88
92
|
end
|
|
93
|
+
|
|
94
|
+
def migrate_legacy_session(root, path)
|
|
95
|
+
legacy_path = File.join(root, ".pray", "session.json")
|
|
96
|
+
return unless File.file?(legacy_path)
|
|
97
|
+
return if File.expand_path(legacy_path) == File.expand_path(path)
|
|
98
|
+
|
|
99
|
+
sessions = load_sessions(path)
|
|
100
|
+
load_sessions(legacy_path).each do |legacy|
|
|
101
|
+
sessions << legacy unless sessions.any? { |entry| entry.server_url == legacy.server_url }
|
|
102
|
+
end
|
|
103
|
+
document = (sessions.length == 1) ? session_to_hash(sessions.first) : {
|
|
104
|
+
"sessions" => sessions.map { |entry| session_to_hash(entry) }
|
|
105
|
+
}
|
|
106
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
107
|
+
write_document(path, document)
|
|
108
|
+
File.delete(legacy_path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def write_document(path, document)
|
|
112
|
+
temporary_path = "#{path}.tmp-#{Process.pid}"
|
|
113
|
+
File.open(temporary_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
|
|
114
|
+
file.write("#{JSON.pretty_generate(document)}\n")
|
|
115
|
+
file.flush
|
|
116
|
+
file.fsync
|
|
117
|
+
end
|
|
118
|
+
File.chmod(0o600, temporary_path)
|
|
119
|
+
File.rename(temporary_path, path)
|
|
120
|
+
end
|
|
89
121
|
end
|
|
90
122
|
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "path_safety"
|
|
5
|
+
require_relative "resource_limits"
|
|
6
|
+
|
|
7
|
+
module Pray
|
|
8
|
+
module TarValidation
|
|
9
|
+
BLOCK_BYTES = 512
|
|
10
|
+
State = Struct.new(:paths, :entry_count, :total_bytes, :pending_path, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def extract!(bytes, output_directory)
|
|
15
|
+
FileUtils.mkdir_p(output_directory)
|
|
16
|
+
validate!(bytes, output_directory: output_directory)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate!(bytes, output_directory: nil)
|
|
20
|
+
offset = 0
|
|
21
|
+
state = State.new(paths: {}, entry_count: 0, total_bytes: 0)
|
|
22
|
+
|
|
23
|
+
while offset + BLOCK_BYTES <= bytes.bytesize
|
|
24
|
+
header = bytes.byteslice(offset, BLOCK_BYTES)
|
|
25
|
+
return if header.bytes.all?(&:zero?)
|
|
26
|
+
|
|
27
|
+
verify_checksum!(header)
|
|
28
|
+
size = parse_octal(header.byteslice(124, 12), "entry size")
|
|
29
|
+
data_start = offset + BLOCK_BYTES
|
|
30
|
+
data_end = data_start + size
|
|
31
|
+
raise Error.integrity("truncated package archive entry") if data_end > bytes.bytesize
|
|
32
|
+
|
|
33
|
+
process_entry(header, bytes.byteslice(data_start, size), size, state, output_directory)
|
|
34
|
+
offset = data_start + ((size + BLOCK_BYTES - 1) / BLOCK_BYTES * BLOCK_BYTES)
|
|
35
|
+
end
|
|
36
|
+
raise Error.integrity("package archive is missing its end marker")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def process_entry(header, data, size, state, output_directory)
|
|
40
|
+
type = header.byteslice(156, 1)
|
|
41
|
+
if type == "x"
|
|
42
|
+
state.pending_path = pax_path(data)
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
if type == "L"
|
|
46
|
+
state.pending_path = c_string(data).delete_suffix("\n")
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
raw_path = state.pending_path || header_path(header)
|
|
51
|
+
state.pending_path = nil
|
|
52
|
+
return if type == "5" && [".", "./"].include?(raw_path)
|
|
53
|
+
|
|
54
|
+
path = PathSafety.validate_archive_member_path!(raw_path)
|
|
55
|
+
return if File.basename(path).start_with?("._")
|
|
56
|
+
if type == "5"
|
|
57
|
+
write_extracted_directory(output_directory, path) if output_directory
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
validate_regular_entry!(type, path, size, state)
|
|
61
|
+
write_extracted_file(output_directory, path, data) if output_directory
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_regular_entry!(type, path, size, state)
|
|
65
|
+
raise Error.integrity("unsupported package archive entry type") unless ["0", "\0"].include?(type)
|
|
66
|
+
|
|
67
|
+
state.entry_count += 1
|
|
68
|
+
if state.entry_count > ResourceLimits::MAX_ARCHIVE_ENTRIES
|
|
69
|
+
raise Error.integrity("package archive exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRIES} entries")
|
|
70
|
+
end
|
|
71
|
+
raise Error.integrity("duplicate package archive path: #{path}") if state.paths.key?(path)
|
|
72
|
+
|
|
73
|
+
state.paths[path] = true
|
|
74
|
+
if size > ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES
|
|
75
|
+
raise Error.integrity("package archive entry exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES} bytes: #{path}")
|
|
76
|
+
end
|
|
77
|
+
state.total_bytes += size
|
|
78
|
+
if state.total_bytes > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
|
|
79
|
+
raise Error.integrity("package archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} decompressed bytes")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def write_extracted_directory(output_directory, path)
|
|
84
|
+
FileUtils.mkdir_p(extracted_destination(output_directory, path))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def write_extracted_file(output_directory, path, data)
|
|
88
|
+
destination = extracted_destination(output_directory, path)
|
|
89
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
90
|
+
File.open(destination, File::WRONLY | File::CREAT | File::EXCL | File::BINARY) do |file|
|
|
91
|
+
file.write(data)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extracted_destination(output_directory, path)
|
|
96
|
+
destination = File.join(output_directory, path)
|
|
97
|
+
unless PathSafety.path_under_root?(output_directory, destination)
|
|
98
|
+
raise Error.integrity("package path escapes package root: #{path}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
destination
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def verify_checksum!(header)
|
|
105
|
+
stored = parse_octal(header.byteslice(148, 6), "checksum")
|
|
106
|
+
sum = 0
|
|
107
|
+
header.bytes.each_with_index do |byte, index|
|
|
108
|
+
sum += (index >= 148 && index < 156) ? 32 : byte
|
|
109
|
+
end
|
|
110
|
+
return if sum == stored
|
|
111
|
+
|
|
112
|
+
raise Error.integrity("invalid package archive checksum")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def header_path(header)
|
|
116
|
+
name = c_string(header.byteslice(0, 100))
|
|
117
|
+
prefix = c_string(header.byteslice(345, 155))
|
|
118
|
+
prefix.empty? ? name : "#{prefix}/#{name}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def c_string(bytes)
|
|
122
|
+
bytes.to_s.split("\0", 2).first.to_s.force_encoding(Encoding::UTF_8).tap do |text|
|
|
123
|
+
raise Error.integrity("invalid package archive path encoding") unless text.valid_encoding?
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_octal(bytes, label)
|
|
128
|
+
value = c_string(bytes).strip
|
|
129
|
+
raise Error.integrity("invalid package archive #{label}") unless value.match?(/\A[0-7]+\z/)
|
|
130
|
+
|
|
131
|
+
value.to_i(8)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def pax_path(data)
|
|
135
|
+
cursor = 0
|
|
136
|
+
path = nil
|
|
137
|
+
while cursor < data.bytesize
|
|
138
|
+
separator = data.index(" ".b, cursor)
|
|
139
|
+
length = separator && Integer(data.byteslice(cursor, separator - cursor), exception: false)
|
|
140
|
+
record_end = cursor + length.to_i
|
|
141
|
+
if !length || length <= 0 || record_end > data.bytesize
|
|
142
|
+
raise Error.integrity("invalid package archive pax header")
|
|
143
|
+
end
|
|
144
|
+
record = data.byteslice(separator + 1, record_end - separator - 2)
|
|
145
|
+
equals = record.index("=".b)
|
|
146
|
+
if equals && record.byteslice(0, equals) == "path"
|
|
147
|
+
value = record.byteslice(equals + 1, record.bytesize - equals - 1).force_encoding(Encoding::UTF_8)
|
|
148
|
+
raise Error.integrity("invalid package archive path encoding") unless value.valid_encoding?
|
|
149
|
+
|
|
150
|
+
path = value
|
|
151
|
+
end
|
|
152
|
+
cursor = record_end
|
|
153
|
+
end
|
|
154
|
+
path
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/pray/verify.rb
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module VerifyProvisioned
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def push_findings(project, report)
|
|
8
|
+
push_exclusive_file_export_findings(project, report)
|
|
9
|
+
Render.planned_provisioned_files(project).each do |file|
|
|
10
|
+
path_text = file.path.to_s.tr("\\", "/")
|
|
11
|
+
absolute = File.join(project.project_root, file.path)
|
|
12
|
+
if File.symlink?(absolute)
|
|
13
|
+
report.findings << VerificationFinding.new(
|
|
14
|
+
kind: "verify_error",
|
|
15
|
+
message: "Provisioned file `#{path_text}` is a symbolic link. Remove the link or choose another destination."
|
|
16
|
+
)
|
|
17
|
+
next
|
|
18
|
+
end
|
|
19
|
+
unless File.file?(absolute)
|
|
20
|
+
report.findings << VerificationFinding.new(
|
|
21
|
+
kind: "verify_error",
|
|
22
|
+
message: "Provisioned file `#{path_text}` from `#{file.package}` is missing. Run `pray install` to materialize it."
|
|
23
|
+
)
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
destination_bytes = File.binread(absolute)
|
|
27
|
+
expected_bytes = Render.expected_provisioned_bytes(file.source, project.manifest.symbols || {})
|
|
28
|
+
next if Hashing.sha256_prefixed(destination_bytes) == Hashing.sha256_prefixed(expected_bytes.b)
|
|
29
|
+
|
|
30
|
+
report.findings << VerificationFinding.new(
|
|
31
|
+
kind: "package_integrity",
|
|
32
|
+
message: "Provisioned file `#{path_text}` no longer matches package `#{file.package}`. Run `pray install` to restore it."
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def push_exclusive_file_export_findings(project, report)
|
|
38
|
+
project.packages.each do |package|
|
|
39
|
+
destination = package.declaration.file
|
|
40
|
+
next unless destination
|
|
41
|
+
|
|
42
|
+
has_file_export = package.selected_exports.any? do |name|
|
|
43
|
+
export = package.spec.exports[name]
|
|
44
|
+
export && export.kind == "file"
|
|
45
|
+
end
|
|
46
|
+
next if has_file_export
|
|
47
|
+
|
|
48
|
+
report.findings << VerificationFinding.new(
|
|
49
|
+
kind: "verify_error",
|
|
50
|
+
message: "Package `#{package.declaration.name}` declares file: \"#{destination}\" but has no selected file export."
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/pray/version.rb
CHANGED
data/lib/pray.rb
CHANGED
|
@@ -19,12 +19,24 @@ require_relative "pray/dotenv"
|
|
|
19
19
|
require_relative "pray/project_context"
|
|
20
20
|
require_relative "pray/environment"
|
|
21
21
|
require_relative "pray/resolve_context"
|
|
22
|
+
require_relative "pray/resolve_source"
|
|
23
|
+
require_relative "pray/resolve_exports"
|
|
22
24
|
require_relative "pray/invocation"
|
|
23
25
|
require_relative "pray/resolve"
|
|
26
|
+
require_relative "pray/compose_dest"
|
|
27
|
+
require_relative "pray/render_patch"
|
|
24
28
|
require_relative "pray/render"
|
|
29
|
+
require_relative "pray/render_dest"
|
|
30
|
+
require_relative "pray/render_layout"
|
|
25
31
|
require_relative "pray/verify_position"
|
|
32
|
+
require_relative "pray/verify_provisioned"
|
|
26
33
|
require_relative "pray/verify"
|
|
34
|
+
require_relative "pray/resource_limits"
|
|
35
|
+
require_relative "pray/http_body"
|
|
36
|
+
require_relative "pray/registry_install"
|
|
37
|
+
require_relative "pray/registry_integrity"
|
|
27
38
|
require_relative "pray/registry"
|
|
39
|
+
require_relative "pray/archive_unpack"
|
|
28
40
|
require_relative "pray/archive"
|
|
29
41
|
require_relative "pray/plan"
|
|
30
42
|
require_relative "pray/git_sources"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pray-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- lib/pray-cli.rb
|
|
98
98
|
- lib/pray.rb
|
|
99
99
|
- lib/pray/archive.rb
|
|
100
|
+
- lib/pray/archive_unpack.rb
|
|
100
101
|
- lib/pray/auth_client.rb
|
|
101
102
|
- lib/pray/cli.rb
|
|
102
103
|
- lib/pray/cli/commands/auth.rb
|
|
@@ -112,6 +113,7 @@ files:
|
|
|
112
113
|
- lib/pray/cli/parse_auth.rb
|
|
113
114
|
- lib/pray/cli/parse_trust.rb
|
|
114
115
|
- lib/pray/cli/suggest.rb
|
|
116
|
+
- lib/pray/compose_dest.rb
|
|
115
117
|
- lib/pray/confess.rb
|
|
116
118
|
- lib/pray/config.rb
|
|
117
119
|
- lib/pray/constraint.rb
|
|
@@ -123,6 +125,7 @@ files:
|
|
|
123
125
|
- lib/pray/format_serialize.rb
|
|
124
126
|
- lib/pray/git_sources.rb
|
|
125
127
|
- lib/pray/hashing.rb
|
|
128
|
+
- lib/pray/http_body.rb
|
|
126
129
|
- lib/pray/invocation.rb
|
|
127
130
|
- lib/pray/literal.rb
|
|
128
131
|
- lib/pray/lockfile.rb
|
|
@@ -141,9 +144,18 @@ files:
|
|
|
141
144
|
- lib/pray/project_context.rb
|
|
142
145
|
- lib/pray/publish.rb
|
|
143
146
|
- lib/pray/registry.rb
|
|
147
|
+
- lib/pray/registry_install.rb
|
|
148
|
+
- lib/pray/registry_integrity.rb
|
|
144
149
|
- lib/pray/render.rb
|
|
150
|
+
- lib/pray/render_content.rb
|
|
151
|
+
- lib/pray/render_dest.rb
|
|
152
|
+
- lib/pray/render_layout.rb
|
|
153
|
+
- lib/pray/render_patch.rb
|
|
145
154
|
- lib/pray/resolve.rb
|
|
146
155
|
- lib/pray/resolve_context.rb
|
|
156
|
+
- lib/pray/resolve_exports.rb
|
|
157
|
+
- lib/pray/resolve_source.rb
|
|
158
|
+
- lib/pray/resource_limits.rb
|
|
147
159
|
- lib/pray/serve.rb
|
|
148
160
|
- lib/pray/serve_federation.rb
|
|
149
161
|
- lib/pray/session.rb
|
|
@@ -151,12 +163,14 @@ files:
|
|
|
151
163
|
- lib/pray/statement_surface.rb
|
|
152
164
|
- lib/pray/substitute.rb
|
|
153
165
|
- lib/pray/sync.rb
|
|
166
|
+
- lib/pray/tar_validation.rb
|
|
154
167
|
- lib/pray/terminal.rb
|
|
155
168
|
- lib/pray/trust.rb
|
|
156
169
|
- lib/pray/trust_feed.rb
|
|
157
170
|
- lib/pray/trust_ops.rb
|
|
158
171
|
- lib/pray/verify.rb
|
|
159
172
|
- lib/pray/verify_position.rb
|
|
173
|
+
- lib/pray/verify_provisioned.rb
|
|
160
174
|
- lib/pray/version.rb
|
|
161
175
|
- pray-cli.gemspec
|
|
162
176
|
homepage: https://pray.kisko.dev
|