3 # A Locator is used to parse and manipulate Keep locator strings.
5 # Locators obey the following syntax:
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
13 # hint-content ::= [A-Za-z0-9@_-]+
15 # Individual hints may have their own required format:
17 # sign-hint ::= "+A" <40 lowercase hex digits> "@" sign-timestamp
18 # sign-timestamp ::= <8 lowercase hex digits>
19 attr_reader :hash, :hints, :size
21 def initialize(hasharg, sizearg, hintarg)
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.
32 rescue ArgumentError => e
37 # Locator.parse! returns a Locator object parsed from the string tok,
38 # raising an ArgumentError if tok cannot be parsed.
40 if tok.nil? or tok.empty?
41 raise ArgumentError.new "locator is nil or empty"
44 m = /^([[:xdigit:]]{32})(\+([[:digit:]]+))?(\+([[:upper:]][[:alnum:]+@_-]*))?$/.match(tok.strip)
46 raise ArgumentError.new "not a valid locator #{tok}"
49 tokhash, _, toksize, _, trailer = m[1..5]
52 trailer.split('+').each do |hint|
53 if hint =~ /^[[:upper:]][[:alnum:]@_-]+$/
56 raise ArgumentError.new "unknown hint #{hint}"
61 Locator.new(tokhash, toksize, tokhints)
64 # Returns the signature hint supplied with this locator,
65 # or nil if the locator was not signed.
67 @hints.grep(/^A/).first
70 # Returns an unsigned Locator.
72 Locator.new(@hash, @size, @hints.reject { |o| o.start_with?("A") })
76 Locator.new(@hash, @size, [])
86 [ @hash, @size, *@hints ].join('+')
88 [ @hash, *@hints ].join('+')
94 # Class to parse a manifest text and provide common views of that data.
95 def initialize(manifest_text)
101 return to_enum(__method__) unless block_given?
102 @text.each_line do |line|
104 next if tokens.empty?
105 stream_name = unescape(tokens.shift)
107 while loc = Locator.parse(tokens.first)
111 yield [stream_name, blocks, tokens.map { |s| unescape(s) }]
116 # Parse backslash escapes in a Keep manifest stream or file name.
117 s.gsub(/\\(\\|[0-7]{3})/) do |_|
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]
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
143 @files = file_sizes.each_pair.map do |(streamname, filename), size|
144 [streamname, filename, size]
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?
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)
169 def exact_file_count?(want_count)
170 files_count(want_count + 1) == want_count
173 def minimum_file_count?(want_count)
174 files_count(want_count) >= want_count
177 def has_file?(want_stream, want_file=nil)
179 want_stream, want_file = File.split(want_stream)
181 each_line do |stream_name, _, filelist|
182 if (stream_name == want_stream) and
183 each_file_spec(filelist).any? { |_, _, name| name == want_file }