- #!/cad/perltools/ruby/ruby-x86-linux/bin/ruby -w
- # p4protect -- process Tensilica-custom perforce protection description files
-
- # Copyright (c) 2005, Tensilica Inc.
- # All rights reserved.
- #
- # Redistribution and use, with or without modification, are permitted provided
- # that the following conditions are met:
- #
- # - Redistributions must retain the above copyright notice, this list of
- # conditions, and the following disclaimer.
- #
- # - Modified software must be plainly marked as such, so as not to be
- # misrepresented as being the original software.
- #
- # - Neither the names of the copyright holders or their contributors, nor
- # any of their trademarks, may be used to endorse or promote products or
- # services derived from this software without specific prior written
- # permission.
- #
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- # POSSIBILITY OF SUCH DAMAGE.
-
- # See `p4protect -h` for usage info.
- #
- # $Id: //depot/other/perforce/server/scripts/triggers/p4protect#34 $
-
- "$Revision: #34 $" =~ /\d+/;
- PROGVERS = $&;
- PROGNAME = "p4protect";
- UTNAME = "unit"; # magic unit test name
-
- require 'pp' # Allows pretty-printing like this: pp my_object
- require 'P4';
- require File.dirname(File.expand_path($0)) + '/P4Triggers';
- #require '../P4Triggers';
- #require '/home/p4admin/bin/triggers/P4Triggers';
-
- #$p4prog = "p4";
- $p4 = nil;
- $debug = false;
- $errors_found = 0;
- $warnings_found = 0;
- $too_many_errors = false;
- $run_tests = false;
- $test_name = nil;
- $displayproc = nil;
-
- # These two are set by setup_users_groups():
- $p4groups = {}; # hash of groupname => [list of users]
- $p4users = {}; # hash of username => substring from 'p4 users' output
-
- $meta_dirname = nil;
- META_FILENAME = 'prot_meta';
-
- PermValues = {'DENY'=>0, 'RO'=>1, 'LOCK'=>2, 'SUBMIT'=>3};
-
-
- # Output a message.
- #
- def displaymsg(msg)
- #$stderr.print "#{PROGNAME}: ";
- if $displayproc
- $displayproc.call(msg)
- else
- $stderr.print msg;
- end
- end
- def error(msg)
- return if $too_many_errors;
- displaymsg "ERROR: #{msg}\n";
- $errors_found += 1;
- if $errors_found >= 20
- displaymsg("ERROR: too many errors (#{$errors_found}), quitting");
- $too_many_errors = true;
- end
- end
- def warn(msg)
- return if $too_many_errors;
- $warnings_found += 1;
- displaymsg "WARNING: #{msg}\n";
- end
-
-
- # Parse a single line into tokens.
- # Handles double-quoted strings properly.
- #
- def parse_line(line, context)
- tokens = [];
- line.chomp!;
- while true
- line.sub!(/^\s+/, ''); # strip leading whitespace
- line.sub!(/^\#.*$/, ''); # strip out comments
- return tokens unless line.length > 0;
- if line.sub!(/^"/, '') # double-quoted?
- if ! line.sub!(/^([^"]*)"/, '')
- error("#{context}: unmatched quote");
- end
- tokens.push($1);
- if line =~ /^\S/
- error("#{context}: unexpected characters following terminating quote");
- end
- else
- if ! line.sub!(/^\S+/, '')
- error("#{context}: internal error parsing '#{line}'");
- end
- tokens.push($&);
- end
- # $stderr.print "Parsing <<#{line}>>\n";
- # return() unless line ne ''; # skip empty lines
- # $stderr.print "Got <<#{line}>>\n";
- # (line);
- # line.split(' ');
- end
- end
-
-
- # Append path to prefix, if path is a relative path.
- # If path is a full path, just return path.
- #
- def pathcat(prefix, path, needfullpath, context)
- return path if path =~ %r|^//|;
- # print "Path is '#{path}'\n";
- error("#{context}: full paths begin with two forward slashes, relative paths begin with none;\n unexpected path '#{path}'") \
- if path =~ %r|^/|;
- # Here, path is a relative path
- return prefix if path.length == 0;
- error("#{context}: full (rooted) pathname expected") \
- if needfullpath and prefix !~ %r|^//|;
- return prefix + ((prefix =~ %r|/$|) ? '' : '/') + path;
- end
-
- # Check that path makes sense, and globs are valid:
- #
- def validatepath(path, context)
- # Catch a common error: FIXME could generalize this
- error("#{context}: superfluous './' prefix in path") if path =~ /^\.\//;
- # Verify that '...' is only at directory boundaries:
- mypath = path.dup;
- if mypath.gsub!(/\.\.\.([^\/])/, '.../*\1') or
- mypath.gsub!(/([^\/])\.\.\./, '\1*/...')
- # FIXME: make this an error:
- warn( "#{context}: tree wildcard '...' not at directory boundary:\n" +
- " '#{path}'\n" +
- " Unlike Perforce, we restrict '...' to the boundaries of the path or '/' characters,\n" +
- " for clarity. There is no loss of expressiveness. Consider rewriting your path as:\n" +
- " '#{mypath}'\n" +
- " (if that is what you intended)");
- end
- # Verify that '/' is only doubled at the beginning:
- error("#{context}: separator '/' can only be doubled at start of path") if path =~ %r(.//);
- # Verify path does not end in '/':
- error("#{context}: path cannot end with '/' separator") if path =~ %r(/$);
- # Verify path does not begin with single '/':
- error("#{context}: path cannot begin with single '/' separator") if path =~ %r(^/[^/]) or path == '/';
- # Verify '*' and '...' are not doubled (note: '*...' is useful and not flagged):
- error("#{context}: glob '*' unnecessarily doubled") if path =~ /\*\*/;
- error("#{context}: glob '...' unnecessarily doubled") if path =~ /\.\.\.\.\.\./;
- error("#{context}: glob '...' unnecessarily followed by '*'") if path =~ /\.\.\.\*/;
- # Verify only valid characters are present:
- # (NOTE: this ensures '@' is not in path, which is required for path_compare())
- error("#{context}: unexpected single quote (') in path: use double quotes around the path to quote spaces") if path =~ /\'/;
- error("#{context}: unexpected character '#{$&}' in path") if path =~ /[^-a-zA-Z0-9_. \'\/*]/;
- end
-
-
- # Create (or modify) group 'g' to contain users/groups listed by name in 'list'.
- #
- def add_group(g, list, fromfile, context)
-
- # Check for ambiguous group/user names:
- if $p4users.detect {|u,v| $p4groups[u]}
- error("#{context}: group and user have the same name '#{u}'");
- end
-
- # Test for invalid group members (Perforce does not check!):
- list.find_all {|w| !($p4users[w] or $p4groups[w]) }.each {|w|
- if fromfile # file-local g_xxx group?
- ug = (w =~ /^G_/) ? "group" : "user"; # member assumed a user or a group?
- error("#{context}: local group #{g}: unrecognized #{ug} '#{w}'");
- list.delete(w);
- else # Perforce-defined G_xxx group?
- if w =~ /^G_/
- # New Perforce groups can be created regardless of protections,
- # so treat non-existent groups as errors:
- error("#{context}: Perforce group #{g}: unrecognized group '#{w}'");
- list.delete(w);
- else
- # Protections must specifically allow each new user before they can be created,
- # so we have to allow new users here, and assume their existence
- # for purposes of protection:
- warn("#{context}: Perforce group #{g}: assuming '#{w}' is a new user");
- $p4users[w] = true;
- end
- end
- }
-
- # Expand groups within group members:
- seen = {}
- users = []
- while w = list.shift
- next if seen[w];
- seen[w] = 1;
- if $p4users[w]
- users.push(w)
- else
- #$stderr.print "SEE #{w} in #{g}\n";
- list.concat($p4groups[w])
- end
- end
- $p4groups[g] = users; # re-assign with expanded list
-
- end
-
- # Read-in user and group definitions from Perforce.
- #
- def setup_users_groups
-
- # Read in existing users.
- # (New users specified in groups will be added later.)
-
- $p4.run_users.each { |user|
- user.sub!(/^(\S+)\s.*$/, '\1');
- if user =~ /^G_/
- warn("#{PROGNAME}: ignoring user '#{user}' incorrectly prefixed with 'G_'"+
- "\n (ask your Perforce administrator to fix this)");
- next;
- end
- unless user =~ /^[a-zA-Z]\w*$/
- warn("#{PROGNAME}: ignoring user '#{user}' not formed like an identifier"+
- "\n (ask your Perforce administrator to fix this)");
- next;
- end
- $p4users[user] = true;
- #$stderr.print "USER #{user}\n";
- }
-
- # Read in existing groups.
-
- $p4.run_groups.each { |g|
- $p4groups[g] = [];
- #$stderr.print "GROUP '#{g}'\n";
- unless g =~ /^G_/
- warn("#{PROGNAME}: ignoring group '#{g}' not prefixed with 'G_'"+
- "\n (ask your Perforce administrator to fix this)");
- next;
- end
- unless g =~ /^G_\w+$/
- warn("#{PROGNAME}: ignoring group '#{g}' containing non-identifier characters"+
- "\n (ask your Perforce administrator to fix this)");
- next;
- end
-
- process_user_group = false
- $p4.run_group('-o', g).join('').split("\n").each { |k|
- if (process_user_group)
- user_group = k.sub(/^\s+(\w+)/, '\1')
- $p4groups[g].push(user_group) if ($1)
- end
- process_user_group = true if (k =~ /^(Users|Subgroups)/)
- }
- }
-
- # Verify groups and expand subgroups within groups
- # (new users specified in groups are added here):
-
- $p4groups.each {|g,list| add_group(g, list, false, PROGNAME); }
-
- # Add '*' group: # FIXME: can't use "user *" in generated protections, so can't allow this for now:
- #add_group('*', $p4users.keys.sort, false, PROGNAME);
- # FIXME TODO instead of this, automatically collapse user lists to groups for shorter protection outputs
-
- #$p4groups.each {|g,list|
- #$stderr.print "GROUP #{g} = ", list.join("+"), " .\n";
- #}
- #exit 0;
- end
-
-
- # Verify that Perforce account is logged in.
- #
- def verify_p4_login
- login_string = $p4.run_login('-s')
- #system("#{$p4prog} login -s > /dev/null 2>&1");
- if (login_string.join("") !~ /ticket expires/)
- error("internal error: Perforce administrator account requires login\n(message is #{login_string})");
- return false;
- end
- return true; # all ok
- end
-
-
- # Verify that the specified comma-separated groups/users are valid.
- # Remove redundant entries.
- # Return array of groups/users.
- #
- def checkwho(who, context)
- seen = {};
- users = {};
- list = [];
- whos = who.split(',');
- oopsie = false;
- while w = whos.shift
- next if seen[w];
- seen[w] = 1;
- unless $p4groups[w] or $p4users[w]
- # oopsie:
- if w =~ /^G_/i
- error("#{context}: unrecognized group name '#{w}'\n" +
- " Valid group names are: " + $p4groups.keys.sort.join(", ") + ".");
- else
- error("#{context}: unrecognized user name '#{w}'\n" +
- " Valid user names are: " + $p4users.keys.sort.join(", ") + ".");
- end
- oopsie = true;
- next
- end
- if w =~ /^g_/ # file-local group name?
- whos = $p4groups[w] + whos; # expand to avoid non-Perforce names
- next;
- end
- list.push(w);
- if $p4users[w]
- users[w] = 1;
- else
- $p4groups[w].each {|u| users[u] = 1 }
- end
- end
- error("#{context}: empty list of users in #{context}") unless list.length > 0 and users.length > 0 and !oopsie;
- return [list, users];
- end
-
-
-
- # Structures used to parse protections:
- #
- ProtEntry = Struct::new(
- :perm, # 'DENY', 'RO', 'LOCK', or 'SUBMIT'
- :dependent, # true (if '?' after perm keyword) or false
- :who, # [array of user and group names, expanded array of users]
- :path, # perforce *full* path name, possibly including globs ('...', '*')
- :startline, # line at which protection was declared
- :perm_overrides # privileges overriden by this entry (hash of perm => 1)
- )
-
- ProtLevel = Struct::new(
- :prefix, # current 'with' prefix at this level
- :prots, # array of ProtEntry and ProtLevel structures
- :startline, # line at which scope started
- :endline # line at which scope ended
- );
-
-
- # Remove identical (non-glob) characters from left of a and b.
- # Return true if only identical characters found,
- # false if different (non-glob) characters found.
- # (@ considered a glob)
- #
- def match_trim_left(a, b)
- while a != '' and b != '' and a !~ /^[*@]/ and b !~ /^[*@]/
- #$stderr.print "a a=#{a} b=#{b} a0=#{a[0]} b0=#{b[0]}\n";
- return false if a[0] != b[0]; # differ at start
- a.slice!(0,1);
- b.slice!(0,1);
- end
- return true;
- end
- # Same, for right side of a and b.
- def match_trim_right(a, b)
- while a != '' and b != '' and a !~ /[*@]$/ and b !~ /[*@]$/
- #$stderr.print "b a=#{a} b=#{b}\n";
- return false if a[-1] != b[-1]; # differ at end
- a.slice!(-1,1);
- b.slice!(-1,1);
- end
- return true;
- end
-
- # Given 'path' (Perforce path not containing globs)
- # and 'pattern' (Perforce path possibly containing globs),
- # return true if path matches pattern, false otherwise.
- # Globs include '*' and '...' (and '@' as synonym for '...', for internal use).
- #
- def path_compare_glob(path, pattern)
- pat = pattern.gsub(/\.\.\./, '@'); # normalize
- pat.gsub!(/([-.])/, '\\\1'); # escape '.' and '-' characters
- pat.gsub!(/\*/, '[^/]*'); # expand '*' globs
- pat.gsub!(/\@/, '.*'); # expand '...' globs
- re = Regexp.new(pat); # build regular expression
- return path =~ re;
- end
-
- # Check for overlap between two path components, a and b.
- # Neither has '...' nor '/'. But they can have '*'.
- # Returns:
- # nil no overlap
- # 0 a equals b
- # 1 a contains b
- # 2 b contains a
- # 3 partial overlap
- #
- def do_component_compare(a, b)
- return 0 if a == b;
- return nil if a !~ /[*@]/ and b !~ /[*@]/; # if no globs, they just differ
- aa = a.dup; bb = b.dup;
- # Compare non-glob characters at beginning and end:
- return nil unless match_trim_left(aa, bb);
- return nil unless match_trim_right(aa, bb);
- return 1 if aa == '*';
- return 2 if bb == '*';
- return path_compare_glob(aa, bb) ? 2 : nil if aa !~ /[*@]/; # globs in bb but not aa ?
- return path_compare_glob(bb, aa) ? 1 : nil if bb !~ /[*@]/; # globs in aa but not bb ?
- return 1 if aa == '*' + bb or aa == bb + '*' or aa == '*' + bb + '*';
- return 2 if bb == '*' + aa or bb == aa + '*' or bb == '*' + aa + '*';
- # Remaining cases are rather odd. Assume partial overlap for now.
- # This is a conservative, fail-safe return value: it may cause a valid
- # protection specification to be rejected, but should not (at least with
- # current code as of March 2005) cause an invalid / illegal protection
- # specification to be accepted. (This statement NOT formally verified)
- # FIXME/TODO: determine whether we can do any better.
- return 3;
- end
- def component_compare(a, b)
- x = do_component_compare(a,b);
- #$stderr.print "'#{a}' <=> '#{b}' is #{x ? x : 'nil'}\n";
- return x;
- end
-
- # Check for overlap between two Perforce paths, a and b.
- # Each path may contain globs ('...' and '*').
- # Returns:
- # nil no overlap
- # 0 a equals b
- # 1 a contains b
- # 2 b contains a
- # 3 partial overlap
- # In other words, if any overlap:
- # bit 0 is set (1) if a covers more than the overlap
- # bit 1 is set (2) if b covers more than the overlap
- #
- def do_path_compare(apath, bpath)
- return 0 if apath == bpath; # paths are identical
-
- aapath = apath.gsub(/\.\.\./, '@'); # convert '...' to '@'
- bbpath = bpath.gsub(/\.\.\./, '@');
- a = aapath.split(%r(/+)); a[0] = '/' if aapath =~ %r(^//);
- b = bbpath.split(%r(/+)); b[0] = '/' if bbpath =~ %r(^//);
-
- overlap = 0;
-
- # Skip over identical initial entries (stop at any '...'):
- while a.length > 0 and b.length > 0 and a[0] !~ /@/ and b[0] !~ /@/
- cmp = component_compare(a[0], b[0]);
- return nil unless cmp; # if different, whole path is different
- overlap |= cmp; # else track overlaps
- a.shift
- b.shift
- end
- # Skip over identical ending entries (stop at any '...'):
- while a.length > 0 and b.length > 0 and a[-1] !~ /@/ and b[-1] !~ /@/
- cmp = component_compare(a[-1], b[-1]);
- return nil unless cmp; # if different, whole path is different
- overlap |= cmp; # else track overlaps
- a.pop
- b.pop
- end
-
- # Done if there are no '...':
- return overlap if a.length == 0 and b.length == 0;
-
- # If any one side is empty, paths are different,
- # because even with '...' non-empty implies at least one more directory:
- return nil if a.length == 0 or b.length == 0;
-
- # Skip over identical starting/ending characters.
- return nil unless match_trim_left(a[0], b[0]);
- return nil unless match_trim_right(a[-1], b[-1]);
-
- # Okay, now we get to some interesting / odd cases.
- # At this point, both arrays are non-empty.
- # At least one starts with a component containing '...', and
- # at least one ends with a component containing '...'.
-
- sega = a.join("/"); # full paths of what's left
- segb = b.join("/");
- return path_compare_glob(sega, segb) ? overlap|2 : nil if sega !~ /[*@]/; # globs in b but not a ?
- return path_compare_glob(segb, sega) ? overlap|1 : nil if segb !~ /[*@]/; # globs in a but not b ?
- return (overlap | 1) if sega=='@'+segb or sega==segb+'@' or sega=='@'+segb+'@';
- return (overlap | 2) if segb=='@'+sega or segb==sega+'@' or segb=='@'+sega+'@';
-
- # Compare what's before any '...':
- aa = a[0].dup; a[0] = aa.sub!(/\**@.*$/, '*') ? $& : ''; # treat any '...' as '*'
- bb = b[0].dup; b[0] = bb.sub!(/\**@.*$/, '*') ? $& : ''; # treat any '...' as '*'
- cmp = component_compare(aa, bb);
- return nil unless cmp; # if different, whole path is different
- overlap |= cmp; # else track overlaps
-
- # Compare what's after any '...':
- aa = a[-1].dup; a[-1] = aa.sub!(/^.*@/, '*') ? $& : ''; # treat any '...' as '*'
- bb = b[-1].dup; b[-1] = bb.sub!(/^.*@/, '*') ? $& : ''; # treat any '...' as '*'
- cmp = component_compare(aa, bb);
- return nil unless cmp; # if different, whole path is different
- overlap |= cmp; # else track overlaps
-
- #$stderr.print "LEFT: '#{a.join('/')}' and '#{b.join('/')}' (overlap is #{overlap})\n";
- sega = a.join("/"); # full paths of what's left
- segb = b.join("/");
- return overlap if sega == segb; # if what remains is identical
- return (overlap | 1) if sega == '@'; # if a is just '...' it's a superset
- return (overlap | 2) if segb == '@'; # if b is just '...' it's a superset
- return (overlap | 1) if sega=='@'+segb or sega==segb+'@' or sega=='@'+segb+'@';
- return (overlap | 2) if segb=='@'+sega or segb==sega+'@' or segb=='@'+sega+'@';
- return 3 if a[0] =~ /^@/ and b[0] =~ /^@/ and a[-1] =~ /@$/ and b[-1] =~ /@$/; # if '...' around everything
-
- # Other cases are too complicated, just assume partial overlap.
- # This is a conservative, fail-safe return value: it may cause a valid
- # protection specification to be rejected, but should not (at least with
- # current code as of March 2005) cause an invalid / illegal protection
- # specification to be accepted. (This statement NOT formally verified)
- # It may cause arcane NO-OP statements to slip through unnoticed.
- # FIXME/TODO: determine whether we can do any better.
- # (Here it's clear we can do better, it's just a question of how far we
- # want to go. I know of patterns that fall through here [eg. see tests],
- # but none of them seem highly likely or interesting. -Marc 3/24/2005)
- warn("assuming partial overlap between '#{apath}' and '#{bpath}'") if $debug; # for debugging
- return 3;
- end
- def path_compare(apath, bpath)
- x = do_path_compare(apath, bpath);
- #$stderr.print "'#{apath}' vs '#{bpath}': #{x ? x : 'nil'}\n";
- return x;
- end
-
-
- # Check for overlap between two protection entries, a and b,
- # both of type ProtEntry (see above).
- # Returns:
- # nil no overlap
- # 0 a equals b
- # 1 a contains b
- # 2 b contains a
- # 3 partial overlap
- #
- def prot_entry_compare(a, b)
- # Compare user hashes:
- a_users = a.who[1].keys.sort
- b_users = b.who[1].keys.sort
- common_users = a_users & b_users
- return nil if common_users.length == 0;
- woverlap = 0;
- woverlap |= 1 if a_users.length > common_users.length;
- woverlap |= 2 if b_users.length > common_users.length;
-
- # Compare paths:
- overlap = path_compare(a.path, b.path);
- return nil unless overlap;
- return (overlap | woverlap);
- end
-
-
- # Check for overlaps between protection entry 'prot' (declared in 'curlevel')
- # and (previous) protection entries in 'level'.
- # Called by parse_file() while parsing protection file.
- #
- # Returns true if check completed due to a superset entry found, false otherwise.
- #
- def level_overlap_check(prot, level, can_override, context)
- # Traverse protections in reverse order, and stop when we encounter a subsuming entry
- # (that completely supersets or equals 'prot').
- #
- level.prots.reverse.each do |prev|
- if prev.class == ProtEntry
- cmp = prot_entry_compare(prot, prev);
- if cmp
- cmpmsg = ["equals",
- "is a subset of",
- "is a superset of",
- "intersects"] [cmp];
- cmpdesc = "#{prev.path}(#{prev.who[0].join('+')}) " +
- "#{cmpmsg} " +
- "#{prot.path}(#{prot.who[0].join('+')})";
- #" in scope that starts at line #{level.startline}"
- # Keep track of what 'prot' overlaps (eg. overrides) to help reduce output protection list:
- prot.perm_overrides[prev.perm] = cmp;
- # For debugging:
- #warn("#{context}: overlap with line #{prev.startline}:\n #{cmpdesc}");
- # Check whether the overlap is okay, if not indicate why:
- if ! prev.dependent and prev.perm != prot.perm
- error( "#{context}: conflicting overlap with independent line #{prev.startline}:" +
- "\n #{cmpdesc}");
- elsif ! can_override # and prev.perm != prot.perm # FIXME: do we allow this 'and' clause or not?
- error( "#{context}: overlap with line #{prev.startline} whose scope has ended:" +
- "\n #{cmpdesc}");
- elsif cmp == 0 or cmp == 1
- error( "#{context}: overlap makes line #{prev.startline} a NO-OP:" +
- "\n #{cmpdesc}");
- elsif cmp == 2 and prev.perm == prot.perm
- error( "#{context}: is a NO-OP subset of line #{prev.startline} with same permissions:" +
- "\n #{cmpdesc}");
- elsif cmp == 2
- # Entry 'prev' is a superset of 'prot', with different permissions.
- # This is normal and okay. At this point however, there is no need
- # need to check 'prot' against entries earlier than 'prev' because we've
- # already made relevant checks when checking 'prev'.
- # Indeed, the search must stop here, otherwise we might hit an entry earlier
- # than 'prev' that is a superset of 'prev' but with same permissions as 'prot'
- # which is also normal but which the above check would flag as an error.
- return true;
- else
- # case 3 (partial overlap / intersection) with different or same permissions,
- # where 'prev' is dependent ('?' specified), are normal and okay.
- end
- end
- else # prev.class == ProtLevel
- #$stderr.print "class is #{prev.class}.\n";
- # Recurse in closed scope. These cannot be overridden:
- return true if level_overlap_check(prot, prev, false, context);
- end
- end
- return false;
- end
-
-
- # Verify whether a protection entry complies with meta protections.
- # Returns true if okay, false (with error already displayed) otherwise.
- #
- def validate_prot(prot, metalist, protfilename, context)
- unless metalist.length > 0
- error("#{context}: missing meta protection information");
- return false;
- end
- metalist.each {|m|
- next unless m.perm == protfilename;
- cmp = prot_entry_compare(m, prot);
- return true if cmp == 0 or cmp == 1; # okay if prot subset of meta entry
- }
- error("#{context}: entry not allowed by meta protection file:\n" +
- " #{prot.perm} #{prot.who[0].join(',')} #{prot.path}");
- return false;
- end
-
- # Return list of users/groups that correspond to '*' for given path and protection file.
- #
- def allowed_users(metalist, protfilename, path)
- seen = {}
- metalist.each {|m|
- next unless m.perm == protfilename;
- overlap = path_compare(m.path, path);
- next unless overlap and (overlap == 0 or overlap == 1);
- # Path is subset, add users/groups:
- m.who[0].each {|w| seen[w] = 1;}
- }
- return seen.keys.sort;
- end
-
-
-
- # Parse a meta protection entry in the master protection file.
- #
- def parse_meta_file(file, filename, metalist, lineref)
- line = lineref[0];
- startline = line;
- metalev = ProtLevel.new('', [], startline); # local use only
-
- while file.length > 0
- line += 1;
- tokens = parse_line(file.shift, line);
- next if tokens.length == 0; # skip empty lines
- context = "#{filename} line #{line}";
- case tokens[0]
- when 'end'
- lineref[0] = line;
- return false; # no errors
-
- when 'meta', 'with', '^', 'DENY', 'RO', 'LOCK', 'SUBMIT'
- error("#{context}: unexpected token '#{tokens[0]}' within 'meta' block");
-
- else # <prot_filename> <who> <path>
- error("#{context}: expected exactly three parameters for meta protection entry:\n <prot_filename>, <who>, <depot_path>") \
- unless tokens.length == 3;
- prot = ProtEntry.new(tokens[0], false, tokens[1], tokens[2], line, {});
- if prot.perm == '^' or prot.who == '^' or prot.path == '^'
- error("#{context}: repeat character '^' not allowed in meta protection line");
- end
- validatepath(prot.path, context);
- prot.path = pathcat('', prot.path, true, context);
- #prot.who = $p4users.keys.join(",") if prot.who == '*';
- prot.who = checkwho(prot.who, context);
-
- # For debugging:
- printf "%3d: meta %7s %16s %s\n", line, prot.perm, prot.who[0].join(","), prot.path if $debug;
-
- metalev.prots = metalist;
- level_overlap_check(prot, metalev, true, context);
- metalist.push(prot);
- end
- end
-
- error("#{context}: missing 'end' for 'meta' starting at line #{startline}");
- lineref[0] = line;
- return true; # unexpected end of file
- end
-
-
- # Parse a protection file.
- # Returns parsed protection in the form of a ProtLevel structure,
- # or nil on error.
- #
- def parse_file(file, protfilename, metalist, is_meta)
- curlevel = ProtLevel.new('', [], 1);
- levels = []; # entry for each level (depth) currently being parsed below curlevel
- lastprot = [];
- prefix = '';
- line = 0; # line number
- while file.length > 0
- line += 1;
- tokens = parse_line(file.shift, line);
- next if tokens.length == 0; # skip empty lines
- context = "#{protfilename} line #{line}";
- # printf "%3d:", line; print " ", tokens.join(', '), ".\n";
-
- case tokens[0]
- when 'begin'
- error("#{context}: unexpected text '#{tokens[1]}' after 'begin'") \
- unless tokens.length == 1 or (tokens.length == 3 and tokens[1] == 'with');
- levels.push(curlevel);
- curlevel = ProtLevel.new(prefix, [], line, nil);
- if tokens.length == 3
- prefix = pathcat(prefix, tokens[2], true, context);
- end
-
- when 'meta'
- unless is_meta
- error("#{context}: meta keyword only allowed in meta (master) protection file");
- return nil;
- end
- unless levels.length == 0
- error("#{context}: meta keyword only allowed at top-level");
- return nil;
- end
- last if parse_meta_file(file, protfilename, metalist, ref = [line]);
- line = ref[0];
-
- when 'end'
- if levels.length == 0
- error("#{context}: unmatched 'end'");
- end
- oldlevel = curlevel
- curlevel = levels.pop;
- oldlevel.endline = line
- curlevel.prots.push(oldlevel);
- prefix = oldlevel.prefix; # restore prefix
-
- when /^(\^|DENY|RO|LOCK|SUBMIT)(\??)$/
- perm = $1;
- dependent = ($2 == '?');
- error("#{context}: expected exactly two parameters after #{perm} keyword") \
- unless tokens.length == 3;
- prot = ProtEntry.new(perm, dependent, tokens[1], tokens[2], line, {});
- prot.path = '' if prot.path == '.'; # this is what pathcat expects
- # Deal with '^' character:
- if lastprot.length == 0 and
- (prot.perm == '^' or prot.who == '^' or prot.path == '^')
- error("#{context}: repeat character '^' not allowed in first protection line");
- end
- prot.perm = lastprot.perm if prot.perm == '^';
- prot.who = lastprot.who if prot.who == '^';
- prot.path = lastprot.path if prot.path == '^';
- lastprot = prot.dup;
- # Other expansions:
- validatepath(prot.path, context);
- prot.path = pathcat(prefix, prot.path, true, context);
- if prot.who == '*'
- prot.who = allowed_users(metalist, protfilename, prot.path).join(",");
- end
- prot.who = checkwho(prot.who, context);
- # Check against meta protections:
- validate_prot(prot, metalist, protfilename, context) or return nil;
-
- printf "%3d: %7s %16s %s\n", line, prot.perm, prot.who[0].join(","), prot.path if $debug;
-
- # Check overlaps:
- unless level_overlap_check(prot, curlevel, true, context)
- levels.reverse.each {|lev| break if level_overlap_check(prot, lev, true, context); }
- end
-
- curlevel.prots.push(prot);
-
- when 'group'
- error("#{context}: expected exactly two parameters (group name, and comma-separate member list) after 'group' keyword") \
- unless tokens.length == 3;
- groupname,members = tokens[1],tokens[2];
- error("#{context}: file-local group name '#{groupname}' invalid or does not start with 'g_' (lowercase)") unless groupname =~ /^g_\w+$/;
- error("#{context}: file-local group name '#{groupname}' already exists") if $p4groups[groupname] or $p4users[groupname];
- add_group(groupname, members.split(","), true, context);
-
- else
- error("#{context}: unrecognized token '#{tokens[0]}'");
- end
- end
-
- $p4groups.delete_if {|g,m| g =~ /^g_/ }; # remove file-local groups
-
- if levels.length > 0
- error("#{context}: missing 'end' for 'begin' at line #{curlevel.startline}");
- end
- curlevel.endline = line;
- if $errors_found > 0
- error("#{protfilename}: #{$errors_found} error(s) found");
- return nil;
- end
-
- return curlevel;
- end
-
-
- # Construct a single Perforce protection entry (line).
- # 'who' is a single user or group (not a list).
- #
- def construct_protection_line(perm,who,path)
- user_group = ($p4groups[who] && who != '*') ? "group" : "user";
- line = " #{perm} #{user_group} #{who} * ";
- # Only quote if needed:
- if path =~ / /
- line += "\"#{path}\""
- else
- line += path
- end
- return line + "\n"
- end
-
-
- # Construct new Perforce protection list from sub-parsetree
- # (as returned by parse_file()).
- #
- def construct_protections_level(parsetree)
- output = "";
-
- parsetree.prots.each do |p|
- if p.class == ProtEntry
- p.who[0].each do |w|
- case p.perm
- when 'DENY' then p4perm = 'list'; prefix = '-'; predeny = false; review = false;
- when 'RO' then p4perm = 'read'; prefix = ''; predeny = true; review = true;
- when 'LOCK' then p4perm = 'open'; prefix = ''; predeny = true; review = true;
- when 'SUBMIT' then p4perm = 'write'; prefix = ''; predeny = false; review = true;
- else error("oopsie, unrecognized permission #{p.perm}.");
- end
- if predeny and p.perm_overrides.keys.find {|pp| PermValues[pp] > PermValues[p.perm] }
- output += construct_protection_line("list", w, "-"+p.path);
- else
- predeny = false;
- end
- if review and (p.perm_overrides.keys.find {|pp| pp == 'DENY' } or
- ! p.perm_overrides.values.find {|pp| pp == 2 } or predeny)
- output += construct_protection_line("review", w, prefix + p.path);
- end
- output += construct_protection_line(p4perm, w, prefix + p.path);
- end
- else
- output += construct_protections_level(p);
- end
- end
- return output;
- end
-
- # Construct new Perforce protection list from a single hierarchical parsetree.
- #
- def construct_protections(protfile, parsetree, metalist)
- output = "";
-
- # First, deny all that this file covers, per the meta file:
- metalist.each do |m|
- next unless m.perm == protfile;
- m.who[0].each do |w|
- output += construct_protection_line("list", w, "-" + m.path);
- end
- end
-
- # Then go through each protection entry:
- output += construct_protections_level(parsetree);
-
- return output;
- end
-
-
-
- ########################### TRIGGER CLASS
-
- # The trigger class, built using Perforce's P4Trigger trigger-wrapper class.
- # The main method in here is validate() which
- # is invoked from the super-class' parse_change() method.
- #
- class MetaProtectTrigger < P4Trigger
-
- # Constructor.
- def initialize()
- # @foo = ...
- super()
- end
-
- # Get protection file contents
- def populate(protfile,file)
- if file
- # It's a new/edited file.
- if file.revisions[0].action == 'delete'
- message("\n Meta protections refer to a file being deleted.\n");
- return nil;
- end
- metachg = "@=#{change.change}";
- else
- # It's an existing file.
- metachg = "#head";
- end
- # FIXME: Using P4 will provide better error handling here:
- #message("*** retrieving #{protfile}#{metachg}.\n");
- # FIXME - fix $p4 to handle -q.
- #contents = `#{$p4prog} print -q #{$meta_dirname}/#{protfile}#{metachg}`;
- contents = $p4.run_print('-q',
- "#{$meta_dirname}/#{protfile}#{metachg}");
- contents = contents.join("");
- unless(contents)
- message("\n Error #{$?} retrieving #{protfile}#{metachg}.\n");
- return nil;
- end
- if contents == ''
- message("\n Empty file or error retrieving #{protfile}#{metachg}.\n");
- return nil;
- end
- return contents;
- end
-
- # Validate submission. This is the main trigger method.
- #
- def my_validate()
-
- # Get a list of protection files in this submission:
- @newfiles = {}
- change.each_file do |file|
- unless file.depot_file =~ %r{^#{$meta_dirname}/(\w+)$}
- message( "\n Please restrict your submission to files in the"+
- "\n #{$meta_dirname} directory. You included:"+
- "\n #{file.depot_file}"+
- "\n which is not allowed in a protection update."+
- "\n" )
- return false; # reject submission
- end
- protname = $1
- @newfiles[protname] = file;
- end
-
- # Process meta protection file:
-
- metafile = populate(META_FILENAME,@newfiles[META_FILENAME]); # get prot_meta file contents
- return false unless metafile; # else reject submission
-
- #$displayproc = proc {|msg| print "FOO: #{msg}";}
- $displayproc = proc {|msg| message(msg)}
- metalist = [];
- parsetrees = {};
- parselist = [];
- unless parse = parse_file(metafile.chomp.split(/\n/), META_FILENAME, metalist, true)
- return false; # reject submission
- end
- parsetrees[META_FILENAME] = parse;
- parselist.push(META_FILENAME);
-
- # Process all files referenced by the meta protection file:
-
- metalist.each do |m|
- protfile = m.perm;
- next if parsetrees[protfile]; # skip if already parsed
- contents = populate(protfile,@newfiles[protfile]); # get contents
- #message("*** got #{protfile}\n");
- return false unless contents; # else reject submission
- unless parse = parse_file(contents.chomp.split(/\n/), protfile, metalist, false)
- return false; # reject submission
- end
- parsetrees[protfile] = parse;
- parselist.push(protfile);
- end
-
- # Now construct new Perforce protection list from parsetrees[]
-
- protections = <<__ENDPROT__;
-
- Protections:
- super user p4admin * //comment/AUTOGENERATED_#{Time.now.asctime.gsub(/ /, '_')}
- __ENDPROT__
- # FIXME: used to have "list user * * -//..." in header but bug in p4 makes this enable arbitrary user creation!
-
- parselist.each do |protfile|
- protections += construct_protections(protfile, parsetrees[protfile], metalist);
- end
-
- protections += <<__ENDPROT__;
- review user p4admin * //...
- super user p4admin * //...
- __ENDPROT__
-
- if change.desc =~ /REJECTTEST/
- message("Submission rejected by submitter request (REJECTTEST keyword found).\n");
- return false; # submission rejected
- end
-
- if $warnings_found > 0 and change.desc !~ /IGNOREWARN/
- message( "\n"+
- "Submission rejected because of warnings (so that you can see them).\n"+
- "Please insert the keyword IGNOREWARN anywhere in your change description\n"+
- "to ignore these warnings and let your submission go through. E.g. do:\n"+
- "\n"+
- " p4 change #{change.change}\n"+
- " (add 'IGNOREWARN' to the Description field)\n"+
- " p4 submit -c #{change.change}\n" );
- return false; # submission rejected
- end
-
- ##################################################
- # COMMIT POINT
- ##################################################
- message("*** constructed protections\n");
- $p4.input(protections)
- $p4.run_protect('-i')
-
- unless $run_tests
- File.open("/home/p4admin/bin/triggers/last_p4protect_output", "w") do |f|
- f.print protections;
- end
- end
-
- message( "Submission rejected because... well, just because.\n" )
- #return false; # submission rejected
- return true; # submission accepted
- end
-
- # Invoke my_validate() above, and try to unlock files on failure:
- def validate()
- valid = my_validate();
- unless valid
- # TEST: when rejecting, unlock all files in the change
- # to avoid content-trigger lock bug:
- change.each_file do |file|
- $p4.run_unlock('-f', file.depot_file);
- # if file.depot_file =~ %r{^#{$meta_dirname}/(\w+)$};
- end
- end
- return valid;
- end
-
-
- end
-
-
- ########################### ARGUMENT PARSING
-
- def usage
- print <<__USAGE__;
- #{PROGNAME} version #{PROGVERS} - process custom protection description files
- usage: #{PROGNAME} [options]
- where options are:
- -h display this message and exit
- -t tname run test <tname>, "#{UTNAME}" means run unit tests
- -d turn on debugging messages
- __USAGE__
- exit 0;
- end
-
-
-
- def process_cmd_args
- require 'getoptlong';
- opts = GetoptLong.new(
- ["--help", "-h", GetoptLong::NO_ARGUMENT ],
- ["--test", "-t", GetoptLong::REQUIRED_ARGUMENT ],
- ["--debug", "-d", GetoptLong::NO_ARGUMENT ]
- );
- opts.each do |opt,arg|
- case opt
- when '--help'
- usage();
-
- when '--debug'
- $debug = true;
-
- when '--test'
- $run_tests = true;
- $test_name = arg
- end
- end
- ARGV.each do |arg|
- # ... process filename argument ...
- end
- end
-
- ########################### MAIN PROGRAM ################################
-
- def manual_main
- verify_p4_login or exit 1;
- setup_users_groups;
-
- metafile = [
- 'meta',
- ' prot_dev G_allusers //depot/main/...',
- ' prot_dev G_allusers //depot/dev/...',
- 'end'
- ];
-
- metalist = [];
- parsetree1 = parse_file(metafile, META_FILENAME, metalist, true);
- parsetree2 = parse_file($stdin.readlines, 'prot_dev', metalist, false);
- #process_paths(parsetree1, parsetree2);
- end
-
- def trigger_main
- $stderr = $stdout; # this really saves a lot of pain getting errors back to the submitter :-)
- print "\n\n"; # for more nicely readable submission reject messages
- $displayproc = proc {|msg| print "#{msg}";}
- verify_p4_login or exit 1;
- setup_users_groups;
-
- trig = MetaProtectTrigger.new;
- return( trig.parse_change( ARGV.shift ) )
- end
-
-
- ########################### CALL MAIN ################################
-
- # When called via trigger:
- if ARGV.length == 1 and ARGV[0] =~ /^\d+$/
- $meta_dirname = '//depot/meta/prot';
- $p4 = P4.new
- $p4.connect
- exit(trigger_main())
- end
-
- process_cmd_args;
-
- # When called from command line:
- unless ($run_tests)
- $meta_dirname = '//depot/meta/prot';
- $p4 = P4.new
- manual_main; # MAIN PROGRAM
- exit 0; # done
- end
-
- ########################### UNIT TESTS ################################
-
- # Override methods in P4 wrapper to get values from the current test.
- # Ruby rocks!
-
- require 'P4'
- require 'test/unit'
-
- # FIXME - make sure these generate assertions
- $saved_assertions = []
-
- class P4
- @saved_input = nil
-
- def xt_unit_test_init(testdir)
- @testdir = testdir
- #p "unit_test_init called with #{@testdir}"
- require @testdir + '/setup.rb'
- end
-
- def xt_strip_comments_and_whitespace(array_in, gold=false)
- array_out = []
- array_in.each { |line|
- line.gsub!(/\s+/, ' ')
- line.sub!(/^ *\#.*$/, '')if gold
- line.gsub!(/\s+$/, '')
- next if line =~ /^\s*$/
- next if line =~ %r{//comment/}
- array_out.push(line)
- }
- return array_out
- end
-
- def xt_write_file(fname, contents)
- print("Writing out #{fname}\n")
- File.open(fname, "w") { |fh|
- fh.print contents
- }
- end
-
- def xt_process_p4_protect
- if (Setup::Ut_expected_test_return_status == 0)
-
- goldfile = @testdir + '/gold_p4_protect'
- goldfile_contents = nil
- if File.readable?(goldfile)
- fh = File.new(goldfile)
- goldfile_contents =
- xt_strip_comments_and_whitespace(fh.readlines, true).join("\n")
- else
- error("Could not read file #{goldfile}\n")
- end
-
- xt_write_file(@testdir + '/actual_p4_protect', @saved_input)
- @stripped_saved_input =
- xt_strip_comments_and_whitespace(@saved_input.split("\n")).
- join("\n")
- if (goldfile_contents == @stripped_saved_input)
- print "Goldfile matches generated 'p4 protect -i'\n"
- else
- error("Goldfile does not match 'p4 protect -i'")
- xt_write_file(@testdir + '/actual_p4_protect.strip',
- @stripped_saved_input)
- xt_write_file(@testdir + '/gold_p4_protect.strip',
- goldfile_contents)
- end
-
- else
- message = "protect -i unexpectedly executed in testcase"
- error(message)
- # FIXME - make sure this causes assertion in caller
- $saved_assertions.push(message)
- end
- end
-
- alias xt_original_input input
- def input(*args)
- if args.length == 1
- @saved_input = args[0]
- else
- error("only support 'input(the_input)")
- end
- end
-
- alias xt_original_run run
- # This fake version of P4:run supports 'run' and 'run_XXX' syntax for P4
- # commands, but not fetch_group.
- def run(*args)
- orig_args = args.clone
- cmd = args.shift
- # run_XXX form puts all args into an array...
- args.flatten!
- # FIXME - fail commands unless connect/login called first.
- # FIXME test that disconnect called before script exits?
- case (cmd)
- when 'connect'
- print "Connecting to nowhere...\n"
- when 'login'
- if (args.length == 1 and args[0] == '-s')
- return ["User foobar ticket expires in 1 hour 3 minutes\n"]
- else
- error("only supporting 'login -s'")
- end
- when 'users'
- error("users takes no args") unless args.length == 0
- return Setup::Ut_p4_users
- when 'groups'
- error("groups takes no args") unless args.length == 0
- return Setup::Ut_p4_group_hash.keys
- when 'group'
- if (args.length == 2 and args[0] == '-o')
- group_form = Setup::Ut_p4_group_hash[args[1]]
- error("non-existent group #{args[1]}") unless group_form
- return [group_form]
- else
- error("only supporting 'group -o <G_name>'")
- end
- when 'print'
- if (args.length == 2 and args[0] == '-q')
- full_fname = args[1].sub(/#.*/, '')
- #p full_fname
- if File.readable?(full_fname)
- fh = File.new(full_fname)
- return [fh.read, ""]
- else
- error("Could not read file #{full_fname}\n")
- return nil
- end
- else
- error("only support 'print -q <full_fname>'")
- return nil
- end
- when 'protect'
- if (args.length == 1 and args[0] == '-i')
- xt_process_p4_protect
- else
- error("only supporting 'protect -i <G_name>'")
- end
-
- when 'describe'
- # FIXME - ignoring changenum OK?
- if (args.length == 2 and args[0] == '-s')
- return [Setup::Ut_p4_describe]
- else
- error("only supporting 'describe -s <changenum>'")
- end
- else
- error("Fake P4 cmd #{orig_args.join(':')}, unimplemented\n")
- end
- end
- end
-
- # Run a single protection test using a set of protection files, a metafile, and
- # a setup.rb file.
- class Check_protection_files < Test::Unit::TestCase # :nodoc:
- #P4_PROTECTION_TESTS_DIR = "p4protect_tests"
-
- def test_protection_file_group
- return if $test_name == UTNAME
- $displayproc = proc {|msg| } # Disable error msg printing
- # FIXME - support only one test, and get it from --test <testname>
- # this is how the script is actually used
- #prot_file_tests = Dir.glob(P4_PROTECTION_TESTS_DIR + "/*")
- #if prot_file_tests.length == 0
- # error("Could not find prot_file tests")
- #end
- print "p4protect test: #{$test_name}\n"
- unless (test(?d, $test_name))
- fail("Could not find test directory #{$test_name}")
- end
-
- $p4 = P4.new
- $meta_dirname = $test_name
- $p4.xt_unit_test_init($test_name)
- # FIXME - pushing on bogus change number; instead, put in test
- ARGV.push(1)
- status = (trigger_main != 0 || $errors_found > 0) ? 1 : 0;
- assert_equal(Setup::Ut_expected_test_return_status, status)
- end
-
- end
-
- class Check_validatepath < Test::Unit::TestCase # :nodoc:
-
- # Helper function for test_star_star
- def star_star_helper(path, expected=1)
- validatepath(path, path)
- assert_equal(expected, $errors_found)
- $errors_found = 0;
- end
-
- # Verify validatepath function
- def test_star_star
- return if $test_name != UTNAME
- print "p4protect test: #{$test_name}\n"
- $errors_found = 0;
-
- save_displayproc = $displayproc;
- $displayproc = proc {|msg| } # Disable error msg printing
-
- star_star_helper('//this-is okay_/.*...txt', 0)
- star_star_helper('relative too', 0)
- star_star_helper('like/this', 0)
-
- star_star_helper('//...a/***')
- star_star_helper('//a**')
- star_star_helper('//aa***bb')
- star_star_helper('//a......b')
- star_star_helper('//a...*...b')
- star_star_helper('//a...*b')
- star_star_helper('//a@b')
- star_star_helper('//a**b')
- star_star_helper('//a#b')
- star_star_helper('//a+b')
- star_star_helper('//a**b/cde/e**f')
- star_star_helper('//a*b/cde/e********f')
- star_star_helper('//donot//doubleslashes')
- star_star_helper('//orend/withone/')
- star_star_helper('/what_s_that_root')
- star_star_helper("//what's this")
-
- star_star_helper('//...**/c', 2)
-
- $displayproc = save_displayproc; # restore printing
- end
- end
-
- # Class for checking def path_compare(apath, bpath)
- # FIXME - test punctuation more fully?
- class Check_path_compare < Test::Unit::TestCase # :nodoc:
-
- def test_no_overlaps # Paths have no overlap ==> nil
-
- return if $test_name != UTNAME
-
- assert_equal(nil,
- path_compare( '//a',
- '//b'))
- assert_equal(nil,
- path_compare( '//a',
- '//A'))
- assert_equal(nil,
- path_compare( '//a*',
- '//b*'))
- assert_equal(nil,
- path_compare( '//a...',
- '//b...'))
- assert_equal(nil,
- path_compare( '//aa',
- '//ab'))
- assert_equal(nil,
- path_compare( '//a*a',
- '//a*b'))
- assert_equal(nil,
- path_compare( '//a...a',
- '//a...b'))
- assert_equal(nil,
- path_compare( '//depot/...foo/o*k',
- '//depot/...bar/o*k'))
- assert_equal(nil,
- path_compare( '//depot/.../foo',
- '//depot/.../afoo'))
- assert_equal(nil,
- path_compare( '//depot/foo',
- '//depot/*/foo'))
- assert_equal(nil,
- path_compare( '//depot/...*/foo',
- '//depot/foo'))
- assert_equal(nil,
- path_compare( '//depot/*.../foo',
- '//depot/foo'))
- assert_equal(nil,
- path_compare( '//depot/...*.../foo',
- '//depot/foo'))
- assert_equal(nil,
- path_compare( '//depot/.../a*foo',
- '//depot/.../b*foo'))
- assert_equal(nil,
- path_compare( '//depot/.../...done',
- '//depot/welldone'))
- assert_equal(nil,
- path_compare( '//depot/welldone',
- '//depot/.../...done'))
- assert_equal(nil,
- path_compare( '//depot/...xy...',
- '//depot/axtz'))
- assert_equal(nil,
- path_compare( '//depot/axtz',
- '//depot/...xy...'))
- assert_equal(nil,
- path_compare( '//depot/*xy*',
- '//depot/axtz'))
- assert_equal(nil,
- path_compare( '//depot/axtz',
- '//depot/*xy*'))
-
- x = path_compare( '//depot/.../*xy*...',
- '//depot/*t*/zyxel/foobar')
- assert((x == nil or x == 3)) # must be nil, currently interpreted as 3 (tricky case)
-
- x = path_compare( '//depot/...xz.../...',
- '//depot/abxyz/*foo')
- assert(x == nil || x == 3) # must be nil, currently interpreted as 3 (tricky case)
- end
-
- def test_paths_identical # Paths are identical ==> 0
-
- return if $test_name != UTNAME
-
- assert_equal(0,
- path_compare( '//a',
- '//a'))
- assert_equal(0,
- path_compare( '//xx',
- '//xx'))
- assert_equal(0,
- path_compare( '//a/bb/ccc/dddd/eeeee/aoeuhts',
- '//a/bb/ccc/dddd/eeeee/aoeuhts'))
- assert_equal(0,
- path_compare( '//1246eygcEOquReaRSwk-_<>',
- '//1246eygcEOquReaRSwk-_<>'))
- assert_equal(0,
- path_compare( '//...foo*/.../a*b/....pdf',
- '//...foo*/.../a*b/....pdf'))
- end
-
- def test_path_a_contains_b # Path a contains b ==> 1
-
- return if $test_name != UTNAME
-
- assert_equal(1,
- path_compare( '//depot/...foo',
- '//depot/*foo'))
- assert_equal(1,
- path_compare( '//depot/...foo',
- '//depot/foo'))
- assert_equal(1,
- path_compare( '//depot/...foo',
- '//depot/...afoo'))
- assert_equal(1,
- path_compare( '//depot/...foo',
- '//depot/...bar...foo'))
- assert_equal(1,
- path_compare( '//depot/aa*bb/foo',
- '//depot/aabb/foo'))
- assert_equal(1,
- path_compare( '//*a/.../b*c',
- '//a/.../b*c'))
- assert_equal(1,
- path_compare( '//*a/.../b...c',
- '//a/.../b...c'))
- assert_equal(1,
- path_compare( '//a/.../b*c*',
- '//a/.../b*c'))
- assert_equal(1,
- path_compare( '//...a/.../b*c',
- '//a/.../b*c'))
- assert_equal(1,
- path_compare( '//...a/.../b...c',
- '//a/.../b...c'))
- assert_equal(1,
- path_compare( '//a/.../b*c...',
- '//a/.../b*c'))
- assert_equal(1,
- path_compare( '//a/.../b...c...',
- '//a/.../b...c'))
- assert_equal(1,
- path_compare( '//depot/...xy...',
- '//depot/axyz'))
- assert_equal(1,
- path_compare( '//depot/*xy*',
- '//depot/axyz'))
-
- x = path_compare( '//depot/.../*yx*...',
- '//depot/*t*/zyxel/foobar')
- assert(x == 1 || x == 3) # must be 1, currently interpreted as 3 (tricky case)
-
- x = path_compare( '//depot/...xy.../...',
- '//depot/abxyz/*foo')
- assert(x == 1 || x == 3) # must be 1, currently interpreted as 3 (tricky case)
- end
-
- def test_path_b_contains_a # Path b contains a ==> 2
-
- return if $test_name != UTNAME
-
- assert_equal(2,
- path_compare( '//depot/foo',
- '//depot/...foo'))
- assert_equal(2,
- path_compare( '//depot/...afoo',
- '//depot/...foo'))
- assert_equal(2,
- path_compare( '//depot/...bar...foo',
- '//depot/...foo'))
- assert_equal(2,
- path_compare( '//depot/aabb/foo',
- '//depot/aa*bb/foo'))
- assert_equal(2,
- path_compare( '//a/.../b*c',
- '//*a/.../b*c'))
- assert_equal(2,
- path_compare( '//a/.../b...c',
- '//*a/.../b...c'))
- assert_equal(2,
- path_compare( '//a/.../b*c',
- '//a/.../b*c*'))
- assert_equal(2,
- path_compare( '//a/.../b...c',
- '//a/.../b...c*'))
- assert_equal(2,
- path_compare( '//a/.../b*c',
- '//...a/.../b*c'))
- assert_equal(2,
- path_compare( '//a/.../b...c',
- '//...a/.../b...c'))
- assert_equal(2,
- path_compare( '//a/.../b*c',
- '//a/.../b*c...'))
- assert_equal(2,
- path_compare( '//a/.../b...c',
- '//a/.../b...c...'))
- assert_equal(2,
- path_compare( '//depot/axyz',
- '//depot/...xy...'))
- assert_equal(2,
- path_compare( '//depot/axyz',
- '//depot/*xy*'))
-
- x = path_compare( '//depot/*t*/zyxel/foobar',
- '//depot/.../*yx*...')
- assert(x == 2 || x == 3) # must be 2, currently interpreted as 3 (tricky case)
-
- x = path_compare( '//depot/abxyz/*foo',
- '//depot/...xy.../...')
- assert(x == 2 || x == 3) # must be 2, currently interpreted as 3 (tricky case)
- end
-
- def test_paths_partially_overlap # Path a, b partially overlap ==> 3
-
- return if $test_name != UTNAME
-
- assert_equal(3,
- path_compare( '//a/*',
- '//*/c'))
- assert_equal(3,
- path_compare( '//a/b/*',
- '//a/*/c'))
- assert_equal(3,
- path_compare( '//*/b/*',
- '//*/*/c'))
- assert_equal(3,
- path_compare( '//a/...*',
- '//*/...b'))
- assert_equal(3,
- path_compare( '//a/*...',
- '//*/c...'))
- assert_equal(3,
- path_compare( '//a.../*',
- '//*.../c'))
- assert_equal(3,
- path_compare( '//...a/*',
- '//...*/c'))
-
- assert_equal(3,
- path_compare( '//depot/...abc...',
- '//depot/...def...'))
- assert_equal(3,
- path_compare( '//depot/...abc*',
- '//depot/...def...'))
- assert_equal(3,
- path_compare( '//depot/...def...',
- '//depot/...abc*'))
- assert_equal(3,
- path_compare( '//depot/...abc*',
- '//depot/*def...'))
- assert_equal(3,
- path_compare( '//depot/*def...',
- '//depot/...abc*'))
- assert_equal(3,
- path_compare( '//depot/abc*',
- '//depot/*def...'))
- assert_equal(3,
- path_compare( '//depot/*def...',
- '//depot/abc*'))
- assert_equal(3,
- path_compare( '//depot/...xy.../zafoo',
- '//depot/abxyz/*foo'))
- assert_equal(3,
- path_compare( '//depot/abxyz/*foo',
- '//depot/...xy.../zafoo'))
- assert_equal(3,
- path_compare( '//depot/.../*yx*...foo*',
- '//depot/*t*/zyxel/foobar...')) # currently with warning
- end
-
-
- end
-