#!/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