X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/a9b4a906124081439326fedc1a1e73fae9c2f40b..074f020c32c55c017433ac5a294a5e0b73b360ad:/sdk/ruby/lib/arvados/keep.rb diff --git a/sdk/ruby/lib/arvados/keep.rb b/sdk/ruby/lib/arvados/keep.rb index e4f62083b0..b2096b5ea0 100644 --- a/sdk/ruby/lib/arvados/keep.rb +++ b/sdk/ruby/lib/arvados/keep.rb @@ -1,3 +1,7 @@ +# Copyright (C) The Arvados Authors. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + module Keep class Locator # A Locator is used to parse and manipulate Keep locator strings. @@ -18,7 +22,7 @@ module Keep # sign-timestamp ::= <8 lowercase hex digits> attr_reader :hash, :hints, :size - LOCATOR_REGEXP = /^([[:xdigit:]]{32})(\+([[:digit:]]+))?(\+([[:upper:]][[:alnum:]+@_-]*))?$/ + LOCATOR_REGEXP = /^([[:xdigit:]]{32})(\+([[:digit:]]+))?((\+([[:upper:]][[:alnum:]@_-]*))+)?\z/ def initialize(hasharg, sizearg, hintarg) @hash = hasharg @@ -35,7 +39,7 @@ module Keep def self.parse(tok) begin Locator.parse!(tok) - rescue ArgumentError => e + rescue ArgumentError nil end end @@ -47,19 +51,19 @@ module Keep raise ArgumentError.new "locator is nil or empty" end - m = LOCATOR_REGEXP.match(tok.strip) + m = LOCATOR_REGEXP.match(tok) unless m raise ArgumentError.new "not a valid locator #{tok}" end - tokhash, _, toksize, _, trailer = m[1..5] + tokhash, _, toksize, _, _, trailer = m[1..6] tokhints = [] if trailer trailer.split('+').each do |hint| - if hint =~ /^[[:upper:]][[:alnum:]@_-]+$/ + if hint =~ /^[[:upper:]][[:alnum:]@_-]*$/ tokhints.push(hint) else - raise ArgumentError.new "unknown hint #{hint}" + raise ArgumentError.new "invalid hint #{hint}" end end end @@ -97,6 +101,9 @@ module Keep end class Manifest + STRICT_STREAM_TOKEN_REGEXP = /^(\.)(\/[^\/\s]+)*$/ + STRICT_FILE_TOKEN_REGEXP = /^[[:digit:]]+:[[:digit:]]+:([^\s\/]+(\/[^\s\/]+)*)$/ + # Class to parse a manifest text and provide common views of that data. def initialize(manifest_text) @text = manifest_text @@ -109,7 +116,7 @@ module Keep stream_name = nil block_tokens = [] file_tokens = [] - line.scan /\S+/ do |token| + line.scan(/\S+/) do |token| if stream_name.nil? stream_name = unescape token elsif file_tokens.empty? and Locator.valid? token @@ -149,12 +156,21 @@ module Keep @text.each_line do |line| stream_name = nil in_file_tokens = false - line.scan /\S+/ do |token| + line.scan(/\S+/) do |token| if stream_name.nil? stream_name = unescape token elsif in_file_tokens or not Locator.valid? token in_file_tokens = true - yield [stream_name] + split_file_token(token) + + file_tokens = split_file_token(token) + stream_name_adjuster = '' + if file_tokens[2].include?('/') # '/' in filename + parts = file_tokens[2].rpartition('/') + stream_name_adjuster = parts[1] + parts[0] # /dir_parts + file_tokens[2] = parts[2] + end + + yield [stream_name + stream_name_adjuster] + file_tokens end end end @@ -215,5 +231,63 @@ module Keep end false end + + # Verify that a given manifest is valid according to + # https://arvados.org/projects/arvados/wiki/Keep_manifest_format + def self.validate! manifest + raise ArgumentError.new "No manifest found" if !manifest + + return true if manifest.empty? + + raise ArgumentError.new "Invalid manifest: does not end with newline" if !manifest.end_with?("\n") + line_count = 0 + manifest.each_line do |line| + line_count += 1 + + words = line[0..-2].split(/ /) + raise ArgumentError.new "Manifest invalid for stream #{line_count}: missing stream name" if words.empty? + + count = 0 + + word = words.shift + count += 1 if word =~ STRICT_STREAM_TOKEN_REGEXP and word !~ /\/\.\.?(\/|$)/ + raise ArgumentError.new "Manifest invalid for stream #{line_count}: missing or invalid stream name #{word.inspect if word}" if count != 1 + + count = 0 + word = words.shift + while word =~ Locator::LOCATOR_REGEXP + word = words.shift + count += 1 + end + raise ArgumentError.new "Manifest invalid for stream #{line_count}: missing or invalid locator #{word.inspect if word}" if count == 0 + + count = 0 + while word =~ STRICT_FILE_TOKEN_REGEXP and ($~[1].split('/') & ['..','.']).empty? + word = words.shift + count += 1 + end + + if word + raise ArgumentError.new "Manifest invalid for stream #{line_count}: invalid file token #{word.inspect}" + elsif count == 0 + raise ArgumentError.new "Manifest invalid for stream #{line_count}: no file tokens" + end + + # Ruby's split() method silently drops trailing empty tokens + # (which are not allowed by the manifest format) so we have to + # check trailing spaces manually. + raise ArgumentError.new "Manifest invalid for stream #{line_count}: trailing space" if line.end_with? " \n" + end + true + end + + def self.valid? manifest + begin + validate! manifest + true + rescue ArgumentError + false + end + end end end