Merge branch '3663-collection-reader-performance'
[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     end
98
99     def each_stream
100       return to_enum(__method__) unless block_given?
101       @text.each_line do |line|
102         tokens = line.split
103         stream_name = unescape(tokens.shift)
104         blocks = []
105         while loc = Locator.parse(tokens.first)
106           blocks << loc
107           tokens.shift
108         end
109         yield [stream_name, blocks, tokens.map { |s| unescape(s) }]
110       end
111     end
112
113     def each_file
114       return to_enum(__method__) unless block_given?
115       each_stream do |streamname, blocklist, filelist|
116         filelist.each do |filespec|
117           start_pos, filesize, filename = filespec.split(':', 3)
118           yield [streamname, filename, filesize.to_i]
119         end
120       end
121     end
122
123     def unescape(s)
124       s.gsub(/\\(\\|[0-7]{3})/) do |_|
125         case $1
126         when '\\'
127           '\\'
128         else
129           $1.to_i(8).chr
130         end
131       end
132     end
133   end
134 end