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.
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(root)
16
- File.join(root, ".pray", "session.json")
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
- File.write(path, JSON.pretty_generate(document))
39
+ write_document(path, document)
38
40
  session
39
41
  end
40
42
 
41
43
  def load_latest(root)
42
- sessions = load_sessions(session_file_path(root))
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
@@ -165,6 +165,8 @@ module Pray
165
165
  )
166
166
  end
167
167
 
168
+ VerifyProvisioned.push_findings(project, report)
169
+
168
170
  [report, rendered_targets, fresh_targets]
169
171
  end
170
172
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pray
4
- VERSION = "1.9.1"
5
- GENERATED_BY = "pray 1.9.1"
4
+ VERSION = "1.10.0"
5
+ GENERATED_BY = "pray 1.10.0"
6
6
  end
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.9.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