Add 'apps/arv-web/' from commit 'f9732ad8460d013c2f28363655d0d1b91894dca5'
[arvados.git] / sdk / ruby / lib / arvados / keep.rb
1 module Keep
2   class Locator
3     # A Locator is used to parse and manipulate Keep locator strings.
4     #
5     # Locators obey the following syntax:
6     #
7     #   locator      ::= address hint*
8     #   address      ::= digest size-hint
9     #   digest       ::= <32 hexadecimal digits>
10     #   size-hint    ::= "+" [0-9]+
11     #   hint         ::= "+" hint-type hint-content
12     #   hint-type    ::= [A-Z]
13     #   hint-content ::= [A-Za-z0-9@_-]+
14     #
15     # Individual hints may have their own required format:
16     #
17     #   sign-hint      ::= "+A" <40 lowercase hex digits> "@" sign-timestamp
18     #   sign-timestamp ::= <8 lowercase hex digits>
19     attr_reader :hash, :hints, :size
20
21     def initialize(hasharg, sizearg, hintarg)
22       @hash = hasharg
23       @size = sizearg
24       @hints = hintarg
25     end
26
27     # Locator.parse returns a Locator object parsed from the string tok.
28     # Returns nil if tok could not be parsed as a valid locator.
29     def self.parse(tok)
30       begin
31         Locator.parse!(tok)
32       rescue ArgumentError => e
33         nil
34       end
35     end
36
37     # Locator.parse! returns a Locator object parsed from the string tok,
38     # raising an ArgumentError if tok cannot be parsed.
39     def self.parse!(tok)
40       if tok.nil? or tok.empty?
41         raise ArgumentError.new "locator is nil or empty"
42       end
43
44       m = /^([[:xdigit:]]{32})(\+([[:digit:]]+))?(\+([[:upper:]][[:alnum:]+@_-]*))?$/.match(tok.strip)
45       unless m
46         raise ArgumentError.new "not a valid locator #{tok}"
47       end
48
49       tokhash, _, toksize, _, trailer = m[1..5]
50       tokhints = []
51       if trailer
52         trailer.split('+').each do |hint|
53           if hint =~ /^[[:upper:]][[:alnum:]@_-]+$/
54             tokhints.push(hint)
55           else
56             raise ArgumentError.new "unknown hint #{hint}"
57           end
58         end
59       end
60
61       Locator.new(tokhash, toksize, tokhints)
62     end
63
64     # Returns the signature hint supplied with this locator,
65     # or nil if the locator was not signed.
66     def signature
67       @hints.grep(/^A/).first
68     end
69
70     # Returns an unsigned Locator.
71     def without_signature
72       Locator.new(@hash, @size, @hints.reject { |o| o.start_with?("A") })
73     end
74
75     def strip_hints
76       Locator.new(@hash, @size, [])
77     end
78
79     def strip_hints!
80       @hints = []
81       self
82     end
83
84     def to_s
85       if @size
86         [ @hash, @size, *@hints ].join('+')
87       else
88         [ @hash, *@hints ].join('+')
89       end
90     end
91   end
92
93   class Manifest
94     # Class to parse a manifest text and provide common views of that data.
95     def initialize(manifest_text)
96       @text = manifest_text
97       @files = nil
98     end
99
100     def each_line
101       return to_enum(__method__) unless block_given?
102       @text.each_line do |line|
103         tokens = line.split
104         next if tokens.empty?
105         stream_name = unescape(tokens.shift)
106         blocks = []
107         while loc = Locator.parse(tokens.first)
108           blocks << loc
109           tokens.shift
110         end
111         yield [stream_name, blocks, tokens.map { |s| unescape(s) }]
112       end
113     end
114
115     def unescape(s)
116       # Parse backslash escapes in a Keep manifest stream or file name.
117       s.gsub(/\\(\\|[0-7]{3})/) do |_|
118         case $1
119         when '\\'
120           '\\'
121         else
122           $1.to_i(8).chr
123         end
124       end
125     end
126
127     def each_file_spec(speclist)
128       return to_enum(__method__, speclist) unless block_given?
129       speclist.each do |filespec|
130         start_pos, filesize, filename = filespec.split(':', 3)
131         yield [start_pos.to_i, filesize.to_i, filename]
132       end
133     end
134
135     def files
136       if @files.nil?
137         file_sizes = Hash.new(0)
138         each_line do |streamname, blocklist, filelist|
139           each_file_spec(filelist) do |_, filesize, filename|
140             file_sizes[[streamname, filename]] += filesize
141           end
142         end
143         @files = file_sizes.each_pair.map do |(streamname, filename), size|
144           [streamname, filename, size]
145         end
146       end
147       @files
148     end
149
150     def files_count(stop_after=nil)
151       # Return the number of files represented in this manifest.
152       # If stop_after is provided, files_count will read the manifest
153       # incrementally, and return immediately when it counts that number of
154       # files.  This can help you avoid parsing the entire manifest if you
155       # just want to check if a small number of files are specified.
156       if stop_after.nil? or not @files.nil?
157         return files.size
158       end
159       seen_files = {}
160       each_line do |streamname, blocklist, filelist|
161         each_file_spec(filelist) do |_, _, filename|
162           seen_files[[streamname, filename]] = true
163           return stop_after if (seen_files.size >= stop_after)
164         end
165       end
166       seen_files.size
167     end
168
169     def exact_file_count?(want_count)
170       files_count(want_count + 1) == want_count
171     end
172
173     def minimum_file_count?(want_count)
174       files_count(want_count) >= want_count
175     end
176
177     def has_file?(want_stream, want_file=nil)
178       if want_file.nil?
179         want_stream, want_file = File.split(want_stream)
180       end
181       each_line do |stream_name, _, filelist|
182         if (stream_name == want_stream) and
183             each_file_spec(filelist).any? { |_, _, name| name == want_file }
184           return true
185         end
186       end
187       false
188     end
189   end
190 end