;#
;# Copyright (c) 1995-1997
;#	Ikuo Nakagawa. All rights reserved.
;#
;# Redistribution and use in source and binary forms, with or without
;# modification, are permitted provided that the following conditions
;# are met:
;#
;# 1. Redistributions of source code must retain the above copyright
;#    notice unmodified, this list of conditions, and the following
;#    disclaimer.
;# 2. Redistributions in binary form must reproduce the above copyright
;#    notice, this list of conditions and the following disclaimer in the
;#    documentation and/or other materials provided with the distribution.
;#
;# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
;#
;# $Id: Scan.pm,v 1.21 1998/10/10 11:31:10 ikuo Exp $
;#
;# For example, to list up all directories and files in
;# servers tree;
;#
;#	use Fan::FTP;
;#	use Fan::Scan;
;#
;#	$ftp = Fan::FTP->new(ftp_server = 'ftp.freebsd.org');
;#	$scan = Fan::Scan->new(
;#		scan_type => 'FTP',
;#		scan_ftp => $ftp,
;#		scan_dir => '/pub/FreeBSD' );
;#	while (defined($p = $scan->get)) {
;#		last if $p->type eq '.';
;#		print $p->path."\n" if $p->type ne 'U';
;#	}
;#
package Fan::Scan;

use strict;
use vars qw($VERSION $LOG
	$n_object $path_gzip $path_compress
	%required %typemap %typerev);

use Carp;
use Fan::Attrib;
use AutoLoader 'AUTOLOAD';

$VERSION = '0.04';
$LOG = 5 unless defined($LOG);

;#
$path_gzip = &findpath("gzip");
$path_compress = &findpath("compress");

;# A special marker for AutoSplit.
1;
__END__

;#
sub findpath ($) {
	my $foo = shift;
	my @path = split(':', $ENV{'PATH'});

	# append some system specific directories...
	push(@path, qw(
		/usr/ucb
		/usr/ccs
		/usr/local/bin
		/usr/gnu/bin
		/usr/contrib/bin
		/opt/local/bin
		/opt/gnu/bin
		/opt/contrib/bin));

	# check existence of the executable.
	for my $d (@path) {
		return "$d/$foo" if -x "$d/$foo" && ! -d _;
	}

	# return itself if executables were not found.
	$foo;
}

;# Destroy Scan object...
;#
sub DESTROY ($) {
	my $self = shift;

	# Close any files first (if we have).
	$self->close;

	# Log message.
	carp("Scan DESTROYING $self") if $LOG > 5;
}

;#
;# search a directory information for the specified directory.
;# usage:
;#	scan_type :
;#		LOCAL ... scan_dir
;#		INDEX ... scan_index
;#		LSLR  ... scan_lslR
;#		FTP   ... scan_ftp, scan_dir
;#		HTTP  ... scan_ftp, scan_dir
;#	$scan = Fan::Scan->new(
;#		scan_type => LSLR,
;#		scan_lslR => "ls-lR.gz" );
;#
sub new ($%) {
	my $this = shift;
	my $class = ref($this) || $this;
	my %params = @_;
	my $self = {};
	bless $self, $class;

	# copy any scan parameter.
	for my $tag (keys %params) {
		$self->{$tag} = $params{$tag} if $tag =~ /^scan_/;
	}

	# Find out scan type.
	my $t = $self->{scan_type};

	# Check types...
	if (!defined($t)) { # not found.
		carp("No scan_type defined"), return undef;
	} elsif ($t eq 'LOCAL') { # scan local directory
		$self->{scan_dir} ne '' && -d $self->{scan_dir}
			or carp("No local directory"), return undef;
	} elsif ($t eq 'FTP') {
		ref($self->{scan_ftp}) # must be some module
			or carp("No scan_ftp"), return undef;
		$self->{scan_dir} ne ''
			or carp("No scan_dir"), return undef;
	} elsif ($t eq 'INDEX') {
		$self->open($self->{scan_index})
			or carp("open(scan_index): $!"), return undef;
	} elsif ($t eq 'LSLR') {
		$self->open($self->{scan_lslR})
			or carp("open(scan_lslR): $!"), return undef;
	} else {
		carp("Scan does not know method"), return undef;
	}

	# cleanup
	delete($self->{endoffile});
	delete($self->{founderror});
	delete($self->{subst});
	delete($self->{hold});
	$self->{magic_array} = {};
	$self->{cur_info} = {};
	$self->{dirs} = [];
	$self->{lslR_curdir} = '.';
	$self->{scan_filter} = [];

	# Log message.
	carp("Scan CREATING $self") if $LOG > 5;

	# Return myself
	$self;
}

;#
;# To get the current working directory, use $p->where.
;# It tells where you are.
;#
sub where ($) {
	my $self = shift;

	exists($self->{walk_curdir}) ? $self->{walk_curdir} : undef;
}

;#
;# $p->start initialize all variables used by `where', `up',
;# and `down'. This routine must be called before you call
;# these routines.
;#
sub start ($) {
	my $self = shift;

	# DEBUG purpose only.
	confess("$self->{walk_curdir} exists in stack")
		if exists($self->{walk_curdir});

	# initial current walk directory is '.'.
	$self->{walk_curdir} = '.';

	# DEBUG mode only
	warn("Scan +DIR=. (start)\n") if $LOG > 7;

	# result is current directory
	$self->{walk_curdir};
}

;#
;# $p->end is useful for the aftercare and validation of
;# directory stack operations.
;# This routine should be called at last of your directory
;# stack operations.
;#
sub end ($) {
	my $self = shift;

	# DEBUG purpose only.
	confess("$self->{walk_curdir}: remains in stack")
		unless ($self->{walk_curdir} eq '.');

	# clean work directory entry
	delete($self->{walk_curdir});

	# DEBUG mode only
	warn("Scan -DIR=. (end)\n") if $LOG > 7;

	# result is current directory
	undef;
}

;#
;# You can go to DOWN stair with $p->down(you_want_to_go).
;# you_want_to_go must no contains a slash(== `/').
;# Additionally, it must not be a null string, `.' or `..'.
;#
sub down ($$) {
	my $self = shift;
	my $dir = shift; # you_want_to_go

	# DEBUG purpose only.
	confess("$dir: illegal directory name")
		if $dir eq '' || $dir eq '.' || $dir eq '..' || $dir =~ /\//;

	# go down one directory
	$self->{walk_curdir} .= '/'.$dir;

	# DEBUG mode only
	warn("Scan +DIR=$self->{walk_curdir}\n") if $LOG > 7;

	# result is current directory
	$self->{walk_curdir};
}

;#
;# You can go to UP stair with $p->up.
;#
sub up ($) {
	my $self = shift;

	# DEBUG purpose.
	confess("$self->{walk_curdir}: no upper directory")
		unless $self->{walk_curdir} =~ s|/[^/]+$||;

	# DEBUG mode only.
	warn("Scan -DIR=$self->{walk_curdir}\n") if $LOG > 7;

	# result is current directory
	$self->{walk_curdir};
}

;#
;# Scan::open
;# Open a file, which may be .gz or .Z compressed file.
;#
sub open ($$) {
	my $self = shift;
	my $file = shift;

	# close first
	$self->close;

	# check filter
	if (ref($file) ne 'GLOB') {
		my $a = $file =~ /\.gz$/ ? "$path_gzip -cd $file|" :
			$file =~ /\.Z$/ ? "$path_compress -cd $file|" :
			-f $file ? $file :
			-f "$file.gz" ? "$path_gzip -cd $file.gz|" :
			-f "$file.Z" ? "$path_compress -cd $file.Z|" :
			$file;

		# local file handle.
		local *FILE;

		# Now, try to open.
		unless (CORE::open(FILE, $a)) {
			carp("CORE::open($a): $!");
			return undef;
		}

# warn("open[".fileno(*FILE)."] $a: o.k.\n");

		# mark as OPENED file.
		$self->{needclose}++;

		# now store to $file.
		$file = *FILE;
	}

	# Register the file handle
	$self->{handle} = $file;

	# Increment # of objects
	$n_object++;

	# Success
	1;
}

;# close file handle if needed.
;#
sub close ($) {
	my $self = shift;

	if ($self->{needclose} && exists($self->{handle})) {
		CORE::close($self->{handle});
	}
	$self->{needclose} = 0;
	delete($self->{handle});
	1;
}

;#
;# Add a filter to Scan object.
;#
sub add_filter ($@) {
	my $self = shift;
	my @args = @_;
	my $c = $args[$[];

	if (ref($c) ne 'CODE') {
		carp("$self->add_filter: $c must be a CODE");
		return undef;
	}
	push(@{$self->{scan_filter}}, \@args);
}

;#
;# clear filter
;#
sub clr_filter ($) {
	my $self = shift;

	$self->{scan_filter} = [];
}

;#
;#
sub perform_filter ($$) {
	my $self = shift;
	my $y = shift;

	# for DEBUG mode only.
	confess("$y must be Attrib")
		unless ref($y) && $y->isa('Fan::Attrib');

	# loop over all scan_filter or finding mismatch.
	for my $filter (@{$self->{scan_filter}}) {
		my($c, @args) = @{$filter};
		# for DEBUG mode only.
		confess("$c must be CODE") unless ref($c) eq 'CODE';
		# try to perform filter...
		&{$c}($y, @args) || return 0;
	}

	# all filter functions return ok
	1;
}

;# Unget means, save current value to hold buffer.
;# Next get operation may returns this.
sub unget ($$) {
	my $self = shift;

	$self->{hold} = shift;
}

;#
;#
sub get ($) {
	my $self = shift;
	my $y;

	# If there is an entry in hold buffer, we return it.
	delete($self->{hold}), return $y if defined($y = $self->{hold});

	# Get one!
	# When a directory object was marked as 'IGNORED' by filter,
	# we should ignore any additional files in that directory.
	my $ignore_nest = 0;
	while (defined($y = $self->getone)) {

		warn($y->to_line."\n") if $LOG >= 8; # in full debug mode

		# Check if we are in an ignored directory.
		if ($ignore_nest > 0) {
			$ignore_nest-- if $y->type eq 'U';
			$y->flag('!'); # and skip this entry.
		}

		# Shall we return this attrib object?
		if ($y->flag eq '!') {
			$self->{ignore_nest}++ if $y->type eq 'D';
		} else {
			last; # 
		}
	}

	# Check error.
	if (!defined($y)) {
		if ($self->{founderror}) {
			# This may be dangerous, so we should terminate
			# any processing.
			croak("Scan::get: error was detected");
		}
	}

	# The result
	$y;
}

;#
;# Get a single entry from index tree.
;#
sub getone ($) {
	my $self = shift;
	my $y;

	# Check end-of-file or error.
	return undef if $self->{endofdata} || $self->{founderror};

	# Check array reference. Error in next statement is critical.
	if (ref($self->{dirs}) ne 'ARRAY') {
		confess("\$self->{dirs} must be ARRAY");
	}

	# Try to parse and store to $y.
	$y = $self->{scan_index} ? $self->get_index : $self->get_misc;

	# Check result.
	unless (ref($y) && $y->isa('Fan::Attrib')) {
		carp("getone: can't get Fan::Attrib object");
		$self->{founderror}++;
		return undef;
	}

	# We prefer an abbrev for the type of this Attrib object.
	my $t = $y->type;

	# If last one is a directory, go down there.
	if (exists($self->{nextdir})) {
		push(@{$self->{dirs}}, $self->{nextdir});
		delete($self->{nextdir}); # clean next directory entry
	}

	# We assume the first entry is the `.' directory.
	if (@{$self->{dirs}} == 0) { # this is the first time
		if (!($t eq 'D' && $y->name eq '.')) {
			carp("First entry must be the \".\" directory.");
			$self->{founderror}++;
			return undef;
		}
		$self->{nextdir} = '.'; # save to hold buffer
		return $y;
	}

	# Type '.' means END-OF-DATA.
	if ($t eq '.') {
		if (@{$self->{dirs}} == 1) { # verify o.k.
			pop(@{$self->{dirs}});
			$self->{endofdata}++;
			return $y;
		}
		if (@{$self->{dirs}} > 1) {
			carp("remaining directories: \""
				.join('/', @{$self->{dirs}})."\"");
		} else {
			carp("No directory in stack");
		}
		$self->{founderror}++;
		return undef;
	}

	# Type 'U' is `Go to the up stair'.
	if ($t eq 'U') { # one dir up
		if (@{$self->{dirs}} < 2) {
			carp("Too many dir UP");
			$self->{founderror}++;
			return undef;
		}
		$y->name(pop(@{$self->{dirs}})); # not return yet
	}

	# Remaining cases require `name' attribute. Check it.
	my $n = $y->name;
	if (!defined($n)) {
		confess("$y: no name found.");
	} elsif ($n =~ m%[\001\377]%) {
		confess("$y: name contains illegal charactor.");
	} elsif ($n =~ m%/([^/]+)$%) { # contains slash(es)
		confess("$y: name contains slash(es).");
	} elsif ($n eq '.' || $n eq '..') {
		confess("$y: \"$n\" is invalid.");
	} else {
		; # o.k.
	}

	# DEBUG mode only.
	# Calculate pathname from directory array.
	my $path = join('/', @{$self->{dirs}}, $n);
	confess($y->path." != $path") unless $y->path eq $path;

	# Type 'U' was already processed.
	if ($t eq 'U') {
		return $y;
	}

	# Type 'D' is a directory. Copy to the nextdir hold buffer.
	if ($t eq 'D') {
		$self->{nextdir} = $n;
		return $y;
	}

	# Type 'L' is a symlink.
	if ($t eq 'L') { # symlink
		if (!defined($y->linkto)) { # linkto is required
			carp("$y: no linkto for symlink");
			$self->{founderror}++;
			return undef;
		}
		return $y;
	}

	# Type 'F' is a normal file.
	if ($t eq 'F') { # normal file
		return $y;
	}

	# Or, unknown type is specified.
	carp("$t: illegal type");
	return undef;
}

;#
sub get_index ($) {
	my $self = shift;
	my $fh = $self->{handle};

	# Check end-of-file or error.
	return undef if $self->{endofdata} || $self->{founderror};

	# DEBUG mode only.
	confess("\$fh must be defined") unless defined($fh);

	# Try to read from the file handle.
	local $_;
	while (defined($_ = <$fh>)) {
		# skip trailing spaces, and ignore comment lines.
		s/\s+$//;
		/^$/ || /^#/ || last;
warn("READ($.): $_\n");
	}

	# Check end-of-file.
	if (!defined($_)) {
		carp("file [".fileno($fh)."] unexpected END-OF-FILE at $.");
		$self->{founderror}++;
		return undef;
	}

	# Let's parse it.
	my $y = Fan::Attrib->new(attr_line => $_);

	# Check result.
	ref($y) && $y->isa('Fan::Attrib')
		or carp("Can't create Attrib from $_"), return undef;

	# where am I?
	my $d;
	if (!defined($d = $self->where)) {
		$y->path('.');
		$self->start;
	} else {
		if ($y->type eq '.') {
			$self->end;
		} elsif ($y->type eq 'U') {
			$y->path($d);
			$self->up;
		} else {
			$y->path($d.'/'.$y->name);
			if ($y->type eq 'D') {
				$self->down($y->name);
			}
		}
	}

	# try filter
	$y->flag('!') unless $self->perform_filter($y);

	# result is Attrib or undef.
	$y;
}

;#
;# Scan with directory structure.
;# We should get directory entries per directory, sort them,
;# and hold until they will be used. A special reference of
;# $self->{magic_array} is used to hold directory entires.
;#
sub get_misc ($) {
	my $self = shift;
	my $y;

	# Try to get the current working directory...
	my $dir;
	if (!defined($dir = $self->where)) {
		$self->start;
		$y = $self->{scan_type} eq 'LOCAL'
		   ? Fan::Attrib->new(attr_path => "$self->{scan_dir}/.")
		   : Fan::Attrib->new(y_type => 'D', y_name => '.');
		if (defined($y)) {
			$y->path('.');
			$y->flag('!') unless $self->perform_filter($y);
		} else {
			carp("Attrib: Can't create object");
		}
		return $y;
	}

	# Delete skipped directories.
	my $cur = $dir;
	my $cur_n = ($cur =~ y|/|\001|);
	my $d;
	for $d (keys %{$self->{magic_array}}) {
		my $tmp = $d;
		my $tmp_n = ($tmp =~ y|/|\001|);
		if ($tmp_n >= $cur_n && $tmp lt $cur) {
			warn("Scan: ($d lt $dir) is true... skipped.\n")
				if $LOG > 5;
			delete($self->{magic_array}->{$d});
		}
	}

	# Check magic_array first.
	my $p = $self->{magic_array}->{$dir};

	# Try to get magic array for this directory
	if (!defined($p)) {
		$p = $self->{scan_type} eq 'LOCAL' ? $self->dir_local($dir)
		   : $self->{scan_type} eq 'LSLR'  ? $self->dir_lslR($dir)
		   : $self->{scan_type} eq 'FTP'   ? $self->dir_ftp($dir)
		   : undef;
		unless (defined($p)) {
			carp("get_misc: can't get array");
			return undef;
		}
	}

	# Now, ($p == $self->{magic_array}->{$dir}) is true.
	if (defined($y = shift(@{$p}))) { # something remains
		$y->path($dir.'/'.$y->name);
		if ($y->type eq 'D') {
			$self->down($y->name);
		}
		unless ($self->perform_filter($y)) {
			$y->flag('!');
			if ($y->type eq 'D') { # setup dummy array
				$self->{magic_array}->{$self->where} = [];
			}
		}
	} else { # or terminate this directory
		delete($self->{magic_array}->{$dir}); # clean
		if ($dir eq '.') {
			$self->end;
			$y = Fan::Attrib->new(y_type => '.');
		} else {
			my $tail = $dir =~ m|([^/]+)$| ? $1 : '.';
			$self->up;
			$y = Fan::Attrib->new(
				y_type => 'U',
				y_name => $tail,
				y_path => $dir );
		}
		if (defined($y)) {
			$y->flag('!') unless $self->perform_filter($y);
		} else {
			confess("Attrib: Can't create object")
		}
	}

	# return Attrib object.
	$y;
}

;#
;# Usage:
;#	array_ref = $scan->dir_local(directory);
;#
sub dir_local ($$) {
	my $self = shift;
	my $dir = shift;
	my $d = "$self->{scan_dir}/$dir";
	my $p = [];

	local(*D);
	opendir(D, $d) or carp("opendir($d): $!"), return undef;
	my @entry = readdir(D);
	closedir(D);

	# for all entries
	my $e;
	for $e (sort @entry) {
		next if $e eq '' || $e eq '.' || $e eq '..';
		my $x = Fan::Attrib->new(
			attr_path => "$d/$e",
			attr_no_checksum => $self->{scan_no_checksum}
		);
		if (defined($x)) {
			# $x->realpath("$d/$e");
			push(@{$p}, $x);
		}
	}

	# return a reference to array.
	$self->{magic_array}->{$dir} = $p;
}

;#
;# Usage:
;#	array_ref = $scan->dir_ftp(directory);
;#
sub dir_ftp ($$) {
	my $self = shift;
	my $dir = shift;
	my $d = "$self->{scan_dir}/$dir";
	my $ftp = $self->{scan_ftp};
	my $hash = undef;

	# try to get magic array for this directory
	if ($self->{scan_dirinfo}) {
		use Fan::DIR;

		my $tmp = "/tmp/dirinfo.$$";
		my $info = Fan::DIR->new();
		if ($ftp->get("$d/.dirinfo", $tmp) && $info->load($tmp)) {
			my $f;
			for $f ($info->index) {
				$hash->{$f} = $info->get($f);
			}
		} elsif ($ftp->fatal) {
			carp("Scan: can't get dirinfo, fatal") if $LOG > 5;
			return undef;
		} else {
			carp("Scan: can't get dirinfo, try nest") if $LOG > 5;
		}
		unlink($tmp);
	}

	# no scan_dirinfo, or fail to load dirinfo.
	if (!defined($hash)) {
		local $_ = $ftp->list($d);

		unless (defined($_)) {
			warn("Scan: can't get list of $d\n") if $LOG > 5;
			return undef;
		}

		$hash = {};
		my $x;
		for $x (split(/\n/)) {
			my $y;
			if (!defined($y = Fan::Attrib->new(attr_list => $x))) {
				warn("$x: could not parse, ignored.\n")
					if $LOG > 6;
			} else {
				$hash->{$y->name} = $y;
			}
		}
	}

	my $x;
	my $p = [];
	for $x (sort keys %{$hash}) {
		if ($x ne '' && $x ne '.' && $x ne '..') {
			$hash->{$x}->realpath("$d/$x");
			push(@{$p}, $hash->{$x});
		}
	}
	$self->{magic_array}->{$dir} = $p;
}

;#
;# Usage:
;#	array_ref = $scan->dir_lslR(directory);
;#
sub dir_lslR ($$) {
	my $self = shift;
	my $dir = shift;
	my $p;

	while (!defined($p = $self->{magic_array}->{$dir})) {
		$self->getline_lslR || return undef;
	}
	$p;
}

;#
;# read a line from ls-lR format file, and parse it.
;# if full entries for a directory $d was found,
;# you can access directory info for that directory from
;# $self->{magic_array}->{$d}.
;#
sub getline_lslR ($){
	my $self = shift;
	my $fh = $self->{handle};
	local $_;

	# check previouse error or end-of-file
	return undef if $self->{endoffile} || $self->{founderror};

	# validate file handle
	confess("Can't find file handle") unless defined($fh);

	# y structure, separator
	my $delim = 0;

	# read a line
	if (!(defined($_ = <$fh>))) {
		$self->{endoffile}++, $delim++;
	} elsif (/^$/) { # null line
		$delim++; # end-of-directory
	} elsif (/^total (\d+)/) { # maybe start of a directory.
		# we simply ignore this.
	} elsif (/^.[-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xtT]\s*/) {
		# normal entry - check this format before directory pattern
		# tested in next statement, because we hate a filename which
		# ends with ':'.
		my $y;
		if (!defined($y = Fan::Attrib->new(attr_list => $_))) {
			warn("$_: could not parse, ignored\n") if $LOG >= 7;
		} else {
			my $n = $y->name;
			if ($n ne '' && $n ne '.' && $n ne '..') {
				$self->{cur_info}->{$n} = $y; # registered.
			}
		}
	} elsif (/:$/) { # new directory
		my $d = $`;
		if (!defined($self->{subst})) {
			my $s = quotemeta($d =~ m%[^/]+$% ? $` : '');
			$self->{subst} = sub {
				local($_) = @_; s|^$s|./|; $_; };
		}
		$self->{lslR_curdir} = &{$self->{subst}}($d);
	} else { # other case?
		warn("$_ unknown format, ignored\n") if $LOG >= 7;
		$self->{founderror}++;
		return undef;
	}

	# directory delimiter
	if ($delim) {
		my $d = $self->{lslR_curdir};
		delete($self->{lslR_curdir});
		my $p = $self->{cur_info};
		$self->{cur_info} = {};
		my $q = $self->{magic_array}->{$d} = [];
### DEBUG BEGIN (for debug only)
		if ($d ne '.' && $d !~ /^\.\//) {
			confess("directory name must begin with '.'\n");
		}
### DEBUG END
		my $n;
		for $n (sort keys %{$p}) {
			push(@{$q}, $p->{$n});
		}
	}

	# success return
	1;
}

;#
;#
sub dump ($) {
	my $self = shift;
	my $count = 0;
	my $d;
	for $d (sort keys %{$self->{magic_array}}) {
		my $p = $self->{magic_array}->{$d};
		my $y;
		for $y (@{$p}) {
			print "dump: ".$y->path."\n";
		}
		$count += @{$p};
	}
	print "dump: total $count entries.\n";
}

;#
;#
sub summary ($) {
	my $self = shift;
	my $count = 0;
	my $d;
	for $d (sort keys %{$self->{magic_array}}) {
		$count += scalar(@{$self->{magic_array}->{$d}});
	}
	print "summary: total $count entries.\n";
}

;#
;# Get an array of ``smallest'' attributes from
;# a list of Scan objects.
;#
sub getcmp ($@) {
	my @array = @_;
	my $end = 0;
	my $z = undef; # the smallest attribute
	my $pp;

	# Search smallest entry, first
	for $pp (@array) {
		my $p;
		if (defined($p = $pp->get)) {
			$pp->unget($p);
			$z = $p if !defined($z) || $p->compare($z) < 0;
		} else {
			$end++;
		}
	}

	# Check end-of-data
	if ($end == @array) {
		return wantarray ? () : undef;
	} elsif ($end > 0) {
		confess("Some unexpected case occured");
	}

	# OK, $z is the smallest one, try to generate result.
	my @result = ();

	# Get an item if smallest values can be found.
	for $pp (@array) {
		my $p = $pp->get;
		if ($z->compare($p)) {
			$pp->unget($p);
			push(@result, undef);
		} else {
			push(@result, $p);
		}
	}

	# Result is an list of attributes.
	@result;
}

;# guessing..., return the Scan object.
;#
sub guess ($$) {
	my $this = shift;
	my $foo = shift;

	# if we already have scanner, return it.
	return $foo if ref($foo) && $foo->isa('Fan::Scan');

	# directory?
	if (-d $foo) {
		return $this->new(scan_type => 'LOCAL', scan_dir => $foo);
	}

	# regular file? if so, it must be an index file.
	if (-f $foo) {
		return $this->new(scan_type => 'INDEX', scan_index => $foo);
	}

	# file glob?
	if (ref($foo) eq 'GLOB') {
		return $this->new(scan_type => 'INDEX', scan_index => $foo);
	}

	# file handle?
	if (defined(fileno($foo))) {
		return $this->new(scan_type => 'INDEX', scan_index => \*{$foo});
	}

	# what is this?
	undef;
}

;# convert file name to a file glob.
;#
sub fileglob ($;$) {
	my $file = shift;
	my $m = @_ ? shift : ''; # mode...

	# do nothing if $file is a file glob.
	return $file if ref($file) eq 'GLOB';

	# check modes.
	$m = '<' if $m eq 'r';
	$m = '>' if $m eq 'w';
	$m = '>>' if $m eq 'a';

	# check modes...
	unless ($m eq '' || $m eq '<' || $m eq '>' || $m eq '>>') {
		carp("fileglob: wrong mode to open operation") if $LOG >= 5;
		return undef;
	}

	# check special filename.
	if ($file eq '' || $file eq '-') { 
		if ($m eq '' || $m eq '<') {
			return \*STDIN;
		} else {
			return \*STDOUT;
		}
	}

	# try to open the target file.
	local *TEMP;
	unless (CORE::open(TEMP, $m.$file)) {
		carp("fileglob: open($m.$file): $!") if $LOG >= 5;
		return undef;
	}

	# debug log...
	warn("fileglob: open[".fileno(*TEMP)."] $file: o.k.\n") if $LOG > 5;

	# result
	*TEMP;
}

;#
;# scan_mklist(output, directory);
;#
;# Return codes:
;#	undef	error was detected.
;#	1	success, the newest list was created.
;#
sub scan_mklist ($$) {
	my $file = shift;
	my $dir = shift;

	my $op = &fileglob($file, 'w');
	my $no = fileno($op);

	if (!defined($no)) {
		carp("scan_mklist: fileglog($file): failure.") if $LOG > 5;
		return undef;
	}

	# debug log
	if (ref($op) eq 'GLOB') { # normal file.
		warn("scan_mklist: use[$no] $file: o.k.\n") if $LOG > 5;
	} else {
		warn("scan_mklist: open[$no] $file: o.k.\n") if $LOG > 5;
	}

	# Try to open local directory tree.
	my $scan = Fan::Scan->guess($dir);

	unless (ref($scan) && $scan->isa('Fan::Scan')) {
		carp("scan_mklist: can't create scanner($dir)");
		return undef;
	}

	# generate lists.
	my $a;
	while (defined($a = $scan->get)) {
		$a->fill_checksum; # checksum is optional.
		print $op $a->to_line."\n";
	}

	# close if needed.
	if (ref($op) eq 'GLOB') {
		warn("scan_mklist: unuse[$no] $file...\n") if $LOG > 5;
	} else {
		warn("scan_mklist: close[$no] $file...\n") if $LOG > 5;
	}

	# this causes the target file be closed automatically.
	undef $op;

	# success.
	1;
}

;#
;# Generate diffs for given two index files.
;# scan_mkdiff(output, old, new)
;#
;# Return codes:
;#	undef	error was detected.
;#	0	diff file was generated, but no change exists.
;#	other	# of changes. diff file was generated.
;#
sub scan_mkdiff ($$$) {
	my $file = shift;
	my $old = shift;
	my $new = shift;

	my $op = &fileglob($file, 'w');
	my $no = fileno($op);

	if (!defined($no)) {
		carp("scan_mkdiff: fileglog($file): failure.") if $LOG > 5;
		return undef;
	}

	# debug log
	if (ref($op) eq 'GLOB') { # normal file.
		warn("scan_mkdiff: use[$no] $file: o.k.\n") if $LOG > 5;
	} else {
		warn("scan_mkdiff: open[$no] $file: o.k.\n") if $LOG > 5;
	}

	my $oldscan = Fan::Scan->guess($old);

	unless(ref($oldscan) && $oldscan->isa('Fan::Scan')) {
		carp("scan_mkdiff: can't create scanner($old)");
		return undef;
	}

	my $newscan = Fan::Scan->guess($new);

	unless(ref($newscan) && $newscan->isa('Fan::Scan')) {
		carp("scan_mkdiff: can't create scanner($new)");
		return undef;
	}

	my @dir = ();	# directry stack
	my $modify = 0;	# modification flag
	my $a;
	my $b;
	while (($a, $b) = $oldscan->getcmp($newscan)) {

		# check difference first.
		if (!defined($a) && !defined($b)) {
			confess("scan_mkdiff: UNEXPECTED CASE");
		} elsif (!defined($a)) {
			$b->flag('+');
		} elsif (!defined($b)) {
			$b = $a; $b->flag('-');
		} elsif (attr_cmp($a, $b)) { # differs
			$b->flag('+');
		} else {
			$b->flag('');
		}

		# print difference if required.
		if ($b->type eq '.') {
			print $op ".\n"; # END-OF-DATA.
		} elsif ($b->type eq 'D' && $b->name eq '.') {
			print $op $b->to_line."\n";
		} elsif ($b->flag ne '') {
			while (@dir) {
				print $op shift(@dir)->to_line."\n";
			}
			print $op $b->to_line."\n";
			$modify++;
		} elsif ($b->type eq 'D') {
			push(@dir, $b);
		} elsif ($b->type eq 'U') {
			if (@dir) {
				pop(@dir);
			} else {
				print $op "U\n";
			}
		} else { # (flag == '' && type !~ /[DU.]/)
			; # ignored
		}
	}

	# close if needed.
	if (ref($op) eq 'GLOB') {
		warn("scan_mkdiff: unuse[$no] $file...\n") if $LOG > 5;
	} else {
		warn("scan_mkdiff: close[$no] $file...\n") if $LOG > 5;
	}

	# this causes the target file be closed automatically.
	undef $op;

	# return # of differences.
	$modify;
}

;#
;# scan_update(output, base, diff [, diff...]);
;#
;# Return codes:
;#	undef	error was detected.
;#	1	success, the newest list was created.
;#
sub scan_update ($$@) {
	my $file = shift;
	my $base = shift;
	my @diff = @_;

	my $op = &fileglob($file, 'w');
	my $no = fileno($op);

	if (!defined($no)) {
		carp("scan_update: fileglog($file): failure.") if $LOG > 5;
		return undef;
	}

	# debug log
	if (ref($op) eq 'GLOB') { # normal file.
		warn("scan_update: use[$no] $file: o.k.\n") if $LOG > 5;
	} else {
		warn("scan_update: open[$no] $file: o.k.\n") if $LOG > 5;
	}

	# Open the index who has maximum number.
	my $basescan = Fan::Scan->guess($base);

	unless(ref($basescan) && $basescan->isa('Fan::Scan')) {
		carp("update: can't create scanner($base)");
		return undef;
	}
	warn("scan_update: base $base: o.k.\n") if $LOG > 5;

	# Initialize array.
	my @array = ();

	# Open step files...
	for my $file (@diff) {
		my $q = Fan::Scan->guess($file);

		unless(ref($q) && $q->isa('Fan::Scan')) {
			carp("update: can't create scanner($file)");
			return undef;
		}
		push(@array, $q);
		warn("scan_update: add $file: o.k.\n") if $LOG > 5;
	}

	# Try merge
	my @a;
	while (@a = $basescan->getcmp(@array)) {
		my $a;
		my $x = undef;

		while (@a) {
			my $a = shift(@a);
			$x = $a if ref($a) && $a->isa('Fan::Attrib');
		}
		if (!defined($x)) {
			confess("scan_update: UNEXPECTED CASE");
		} elsif ($x->flag ne '-') {
			$x->flag('');
			print $op $x->to_line."\n";
		}
	}

	# close if needed.
	if (ref($op) eq 'GLOB') {
		warn("scan_update: unuse[$no] $file...\n") if $LOG > 5;
	} else {
		warn("scan_update: close[$no] $file...\n") if $LOG > 5;
	}

	# this causes the target file be closed automatically.
	undef $op;

	# Success return.
	1;
}

;# Compare two Attrib objects.
;#
sub attr_cmp ($$) {
	my $a = shift;
	my $b = shift;
	my $tag;
	my $val;

	# check each items...
	while (($tag, $val) = each %{$a}) {
		next if $tag !~ /^y_/;
		return -1 if $b->{$tag} ne $val;
	}

	# or same
	0;
}

;# end of Fan::Scan module
