vetra 0.1.0.pre.alpha.4

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.
@@ -0,0 +1,78 @@
1
+ require "find"
2
+ require "pathname"
3
+
4
+ module Vetra
5
+ module Graph
6
+ class Manifest
7
+ REQUIRED = %w[config/app.rb Gemfile Gemfile.lock .ruby-version].freeze
8
+ attr_reader :root, :entries
9
+
10
+ def initialize(root, extra_paths: [])
11
+ @root = Pathname.new(root).realpath
12
+ @bytes = {}
13
+ paths = REQUIRED + extra_paths
14
+ %w[app lib config].each do |directory|
15
+ base = @root.join(directory)
16
+ next unless base.exist? || base.symlink?
17
+ Find.find(base.to_s) do |path|
18
+ relative = Pathname.new(path).relative_path_from(@root).to_s
19
+ if Pathname.new(path).basename.to_s.match?(/\A(?:secrets|credentials)(?:\.|\z)|\A\.env|\.(?:key|pem|enc)\z/)
20
+ Find.prune if File.directory?(path)
21
+ next
22
+ end
23
+ checked_path(relative)
24
+ if File.symlink?(path) && File.directory?(path)
25
+ raise Error.single("input_symlink", "Diretórios de entrada não podem ser links: #{relative}.")
26
+ end
27
+ paths << relative if File.file?(path)
28
+ end
29
+ end
30
+ @entries = paths.uniq.sort.map do |path|
31
+ @bytes[path] = checked_path(path).binread
32
+ { "path" => path, "digest" => "sha256:#{Digest::SHA256.hexdigest(@bytes.fetch(path))}" }
33
+ end
34
+ Data.freeze(@entries)
35
+ rescue Errno::ENOENT, Errno::ENOTDIR => e
36
+ raise Error.single("input_missing", "Entrada ausente; confira config/app.rb, Gemfile, Gemfile.lock e .ruby-version (#{e.class}).")
37
+ end
38
+
39
+ def read(path)
40
+ @bytes.fetch(path).dup.force_encoding(Encoding::UTF_8)
41
+ end
42
+
43
+ def digest_for(path)
44
+ @entries.find { |entry| entry['path'] == path }.fetch("digest")
45
+ end
46
+
47
+ def source(path, line)
48
+ absolute = Pathname.new(path).expand_path
49
+ relative = absolute.relative_path_from(root).to_s
50
+ checked_path(relative)
51
+ { "path" => relative, "line" => line }
52
+ end
53
+
54
+ def verify!(document)
55
+ unless entries == document.fetch("inputs")
56
+ raise Error.single("inputs_changed", "Fontes adicionados, removidos ou alterados; execute graph:build no build de release.", pointer: "/inputs")
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def checked_path(path)
63
+ pieces = path.split("/", -1)
64
+ if path.empty? || path.start_with?("/") || path.match?(/\A[A-Za-z]:|[\\\x00-\x1f]/) || pieces.any? { |part| %w[. ..].include?(part) || part.empty? }
65
+ raise Error.single("input_path", "Use caminho relativo sem travessia: #{path.inspect}.")
66
+ end
67
+ if pieces.any? { |part| part == ".vetra" || part.match?(/\A(?:secrets|credentials)(?:\.|\z)|\A\.env|\.(?:key|pem|enc)\z/) }
68
+ raise Error.single("input_path", "Cache e segredos não são entradas da IR.")
69
+ end
70
+ actual = root.join(path).realpath
71
+ unless actual.to_s.start_with?("#{root}/")
72
+ raise Error.single("input_path", "Entrada escapa da raiz do projeto: #{path}.")
73
+ end
74
+ actual
75
+ end
76
+ end
77
+ end
78
+ end