#!/usr/bin/perl -w

# $Id: ClamdOmitScan.pl,v 1.30 2009/04/25 06:31:56 okamura Exp $

require 5.8.0;
use strict;
use utf8;
#-------------------------------------------------------------------------------
# モジュール
#-------------------------------------------------------------------------------
# システム
use Getopt::Long	qw(:config no_ignore_case);
use Pod::Usage;
use Cwd	qw(realpath);
use File::Find;
use File::Basename;
use File::Spec;
use Time::HiRes	qw(time alarm);
use File::Path;
use Fcntl	qw(:flock);
use IO::Socket;
use Time::Local;
eval {
	require Sys::Filesystem;
};
use File::Temp qw(tempdir);

# プロジェクト

#-------------------------------------------------------------------------------
# グローバル変数宣言
#-------------------------------------------------------------------------------
use vars qw(
	$ProgramName
	$Version
	$Prefix
	%Conf
	%ClamdConf
	%Options
	%Summary
	%ActionSwitch
	%MonthName2Month
	%ExitStatus
);

#-------------------------------------------------------------------------------
# 定数
#-------------------------------------------------------------------------------
# プログラム名
$ProgramName = 'ClamdOmitScan';

# バージョン
$Version = 'ClamdOmitScan.pl v1.3';

# プレフィクス
$Prefix = '/usr/local';

# 月名から月番号へのハッシュ
#	月番号は 0 から始まる
%MonthName2Month = (
	Jan	=> 0,
	Feb	=> 1,
	Mar	=> 2,
	Apr	=> 3,
	May	=> 4,
	Jun	=> 5,
	Jul	=> 6,
	Aug	=> 7,
	Sep	=> 8,
	Oct	=> 9,
	Nov	=> 10,
	Dec	=> 11,
);

# 終了ステータス
%ExitStatus = (
	NoError			=> 0,
	ViruesFound		=> 1,
	SomeErrors		=> 2,
	UknownOpts		=> 40,
	BadParams		=> 41,
	BadArguments	=> 42,
	UnsupportedOpts	=> 43,
	ScanError		=> 50,
	MaintainError	=> 51,
);

#-------------------------------------------------------------------------------
# サブルーチン
#-------------------------------------------------------------------------------
#--- ロギング ----
# 出力
# @param $format フォーマット
# @param @args フォーマットの引数
sub Output {
	my	($format, @args) = @_;
	my	$message = sprintf $format, @args;

	if (0 < scalar(@{$ActionSwitch{outputStream}})) {
		foreach my $stream (@{$ActionSwitch{outputStream}}) {
			print {$stream} $message;
		}
	}
	elsif ($Conf{stdout}) {
		print $message;
	}
	else {
		print STDERR $message;
	}
}

# エラー出力
# @param $format フォーマット
# @param @args フォーマットの引数
sub Error {
	my	($format, @args) = @_;

	$format = 'ERROR: '.$format . "\n";
	Output($format, @args);
	$Summary{count}->{error}++;
}

# 警告出力
# @param $format フォーマット
# @param @args フォーマットの引数
sub Warn {
	my	($format, @args) = @_;

	$format = 'WARNING: '.$format . "\n";
	Output($format, @args);
	$Summary{count}->{error}++;
}

# 情報出力
# @param $format フォーマット
# @param @args フォーマットの引数
sub Info {
	if (!$Conf{quiet} and $Conf{verbose}) {
		my	($format, @args) = @_;

		$format = 'INFO: '.$format . "\n";
		Output($format, @args);
	}
}

# デバッグ出力
# @param $format フォーマット
# @param @args フォーマットの引数
sub Debug {
	if (!$Conf{quiet} and $Conf{verbose}) {
		my	($format, @args) = @_;

		$format = 'DEBUG: '.$format . "\n";
		Output($format, @args);
	}
}

# 非サポートオプション警告
# @param $name オプション名
# @param $key 値
#	$value が undef ではない場合はハッシュのキー
# @param $value 値
sub WarnUnsupportedOption {
	my	($name, $key, $value) = @_;

	if ($Conf{'error-unsupported'}) {
		Error 'ERROR: Unsupported option %s', $name;
	}
	elsif ($Conf{'warn-unspported'}) {
		Warn 'Ignoring option %s', $name;
	}
	if ($Conf{'error-unsupported'}) {
		exit $ExitStatus{UnsupportedOpts};
	}
}

# ファイル名のエスケープ
# @param $path	ファイル名
sub EscapedPath {
	my	($path) = @_;

	$path =~ s/([\x00-\x1F\x7f])/?/gs;
	return $path;
}

# 除外レポート
# @param $path パス
sub ReportExcluded {
	unless ($Conf{infected}) {
		my	($path) = @_;

		Output "%s: Excluded\n", EscapedPath($path);
	}
}

# チェック済みレポート
# @param $path パス
sub ReportChecked {
	my	($path) = @_;

	Output "%s: Checked\n", EscapedPath($path)	unless ($Conf{infected});
	$Summary{count}->{files}->{checked}++;
	$Summary{count}->{data}->{checked} += $ActionSwitch{statCache}->[7];
}

# 応答レポート
# @param $path パス
# @param %result 次の形式のスキャン結果
#	(
#		OK	=> [ファイル名, ...],
#		FOUND	=> [[ファイル名, ウィルス名], ...],
#		ERROR	=> [[ファイル名, エラーメッセージ], ...],
#		UNKNOWN	=> [[ファイル名, メッセージ], ...],
#	)
sub ReportResult {
	my	($path, %result) = @_;

	foreach (@{$result{OK}}) {
		ReportOkResult($_);
	}

	foreach (@{$result{FOUND}}) {
		ReportFoundResult($_->[0], $_->[1]);
	}

	foreach (@{$result{ERROR}}) {
		ReportErrorResult($_->[0], $_->[1]);
	}

	foreach (@{$result{UNKNOWN}}) {
		ReportUnknownResult($_->[0], $_->[1]);
	}

	if (scalar(@{$result{FOUND}})) {
		if ($Conf{remove}) {
			unless (unlink $path) {
				Warn 'Can\'t unlink: %s', EscapPath($path);
			}
		}
		else {
			MoveInfected($path);
		}
	}
}

# 出力ファイル名の変換
# @param $path	実際にスキャンしたファイル名
# @return 置き換え後のファイル名
sub ReplaceFName {
	my	($path) = @_;

	if (
		defined($ActionSwitch{targetPrefix})
			and
		defined($ActionSwitch{targetPrefix}->{prefix})
			and
		defined($ActionSwitch{targetPrefix}->{prefixLen})
			and
		defined($ActionSwitch{targetPrefix}->{replace})
			and
		substr($path, 0, $ActionSwitch{targetPrefix}->{prefixLen})
			eq
		$ActionSwitch{targetPrefix}->{prefix}
	) {
		$path = $ActionSwitch{targetPrefix}->{replace}
				.
			substr($path, $ActionSwitch{targetPrefix}->{prefixLen})
		;
	}

	return $path;
}

# クリーン応答レポート
# @param $path パス
sub ReportOkResult {
	my	($path) = @_;
	my	$reportPath = ReplaceFName($path);

	Output "%s: OK\n", EscapedPath($reportPath)	unless ($Conf{infected});
	$Summary{count}->{files}->{scaned}++;
	$Summary{count}->{data}->{scaned}
		+= $ActionSwitch{statCache}->[7]
	;
	UpdateScanData($ActionSwitch{ScanDataHandle}, $reportPath);
}

# 感染応答レポート
# @param $path パス
# @param $virus ウィルス名
sub ReportFoundResult {
	my	($path, $virus) = @_;
	my	$reportPath = ReplaceFName($path);

	Output "%s: %s FOUND\n", EscapedPath($reportPath), $virus;
	$Summary{count}->{files}->{scaned}++;
	$Summary{count}->{data}->{scaned}
		+= $ActionSwitch{statCache}->[7]
	;
	$Summary{count}->{files}->{infected}++;

	print STDERR "\x07"	if ($Conf{bell});
}

# エラー応答レポート
# @param $path パス
# @param $message エラーメッセージ
sub ReportErrorResult {
	my	($path, $message) = @_;
	my	$reportPath = ReplaceFName($path);

	Output "%s: %s ERROR\n", EscapedPath($reportPath), $message;
	$Summary{count}->{error}++;
}

# 不明応答レポート
# @param $path パス
# @param $message メッセージ
sub ReportUnknownResult {
	my	($path, $message) = @_;
	my	$reportPath = ReplaceFName($path);

	Output "%s: %s\n", EscapedPath($reportPath), $message;
	$Summary{count}->{error}++;
}

# 移動レポート
sub ReportMoved {
	unless ($Conf{infected}) {
		my	($src, $dst) = @_;

		Output "%s: %s to '%s'\n",
			$src, defined($Conf{move}) ? 'moved' : 'copied', $dst
		;
	}
}


#--- スキャンデータの操作 ----
# スキャンデータのメインテナンス
# @return 成功/失敗
sub MaintainScanData {
	my	$ok = 1;

	if ($ok) {
		$ok = InitializeOutputStream();
	}
	if ($ok) {
		$ActionSwitch{ScanDataHandle} = OpenScanData($Conf{data}, $Conf{lock});
		$ok = defined($ActionSwitch{ScanDataHandle});
	}
	chmod $Conf{'perm-root'}, $ActionSwitch{ScanDataHandle}->{basePath};
	if ($ok) {
		$ok = SlimScanData(
			$ActionSwitch{ScanDataHandle}, '/', \&_SlimScanData4Maintain
		);
	}
	if ($ok) {
		CloseScanData($ActionSwitch{ScanDataHandle});
		$ActionSwitch{ScanDataHandle} = undef;
	}

	return $ok;
}

# スキャンデータのオープン
# @param $scanDataPath スキャンデータのパス
# @param $lock ロックモード
#	偽: シェアロック, 真: 排他ロック
# @return スキャンデータのハンドル
#	失敗したときは undef が返される。
sub OpenScanData {
	my	($scanDataPath, $lock) = @_;
	my	$handle = { fileHandle => undef, basePath => realpath($scanDataPath) };

	# $scanDataPath が存在しないディレクトリのサブディレクトリの場合
	# realpath が undef を返してしまう。
	unless (defined $handle->{basePath}) {
		# そこで catfile で整形しておく。
		$handle->{basePath} = File::Spec->catfile($scanDataPath);
	}

	# 絶対に root ディレクトリを使用しては駄目!
	if ($handle->{basePath} eq File::Spec->rootdir()) {
		Error '%s is not allow as scanning data directory.',
			EscapPath($handle->{basePath})
		;
		return undef;
	}

	unless (-e $handle->{basePath}) {
		# スキャンデータディレクトリを作る。
		my	@created;

		eval {
			@created = mkpath($handle->{basePath}, 0,  $Conf{'perm-root'})
		};
		if ($@) {
			Error 'Can\'t create directory: %s: %s',
				$@, EscapedPath($handle->{basePath})
			;
			return undef;
		}
		chmod $Conf{'perm-root'}, $handle->{basePath};
		@created = map { EscapedPath($_) } @created;
		Debug "Create directories: %s", "@created";
	}

	my	$oldUmask = umask ($Conf{'perm-lock'} ^ 07777);
	my	$lockFile = "$handle->{basePath}.lock";
	my	$ret;

	$ret = open $handle->{fileHandle}, '>', $lockFile;
	umask $oldUmask;
	unless ($ret) {
		Error '%s: %s', "$!", EscapedPath($lockFile);
		return undef;
	}
	chmod $Conf{'perm-lock'}, $lockFile;

	Debug "Lock (%s) %s ...",
		$lock ? 'exclusive' : 'shared', EscapedPath($lockFile)
	;
	flock $handle->{fileHandle}, $lock ? LOCK_EX : LOCK_SH;
	Debug "Done.";

	$ActionSwitch{ScanDataBasePathLen} = length($handle->{basePath});

	return $handle;
}

# スキャンデータのクローズ
# @param $handle スキャンデータのハンドル
# @return 成功/失敗
#	undef が返された場合はオープンされていないことを表す。
sub CloseScanData {
	my	($handle) = @_;
	my	$lockFile;
	my	$ret = 1;

	return undef	unless ($handle->{fileHandle});

	$lockFile = "$handle->{basePath}.lock";

	unless (flock $handle->{fileHandle}, LOCK_UN) {
		$ret = 0;
		Warn 'Can\'t unlock a file: %s: %s', "$!", EscapedPath($lockFile);
	}
	Debug '%s is unlocked.', EscapedPath($lockFile);

	unless (close $handle->{fileHandle}) {
		$ret = 0;
		Warn 'Can\'t close a file: %s: %s', "$!", EscapedPath($lockFile);
	}

	# ロックファイルは削除しない
#	unless (unlink $lockFile) {
#		$ret = 0;
#		Warn 'Can\'t unlink a file: %s: %s', "$!", EscapedPath($lockFile);
#	}

	return $ret;
}

# スキャンデータのスリム化の wanted 関数
sub _SlimScanData {
	my	$dataPath = $File::Find::name;
	my	$realPath = substr($dataPath, $ActionSwitch{ScanDataBasePathLen});

	if (defined $ActionSwitch{targetPrefix}) {
		$realPath = ReplaceFName($realPath);
	}

	unless ($realPath eq '') {
		if (-d $dataPath) {
			if (-d $realPath) {
				1;
			}
			elsif (-f $realPath and $Conf{dmg} && IsDiskImageFile($realPath)) {
				my	$mountPoint;
				my	@umountArgs;
				my	$check;

				$check = defined(
					$mountPoint = tempdir('ClamdOmitScan-XXXXXXXXXX',
						CLEANUP => 1, DIR => File::Spec->tmpdir
					)
				);
				if ($check) {
					$mountPoint = realpath($mountPoint);
					$check =
						0 < scalar(
							@umountArgs = MountDiskImage(
								$realPath, $mountPoint
							)
						)
							and
						$umountArgs[0] ? 1 : 0
					;
					if ($check) {
						my	$nextRealPath = substr($dataPath, $ActionSwitch{ScanDataBasePathLen});

						local	$ActionSwitch{targetPrefix} = {
							prefix		=> $nextRealPath,
							prefixLen	=> length($nextRealPath),
							replace		=> $mountPoint,
						};

						find(\&_SlimScanData, $dataPath);
						$check = UnmountDiskImage(@umountArgs);
					}
					rmdir $mountPoint;
				}
				unless ($check) {
					Error "$!";
				}
				$File::Find::prune = 1;
			}
			else {
				unless (rmtree($dataPath, 0, 0)) {
					Warn '%s: %s', "$!", EscapedPath($dataPath);
				}
				$File::Find::prune = 1;
			}
		}
		elsif (-f $dataPath) {
			unless (
				-f $realPath and !$Conf{dmg} || !IsDiskImageFile($realPath)
			) {
				unless (unlink($dataPath)) {
					Warn '%s: %s', "$!", EscapedPath($dataPath);
				}
			}
		}
		else {
			Warn 'This is not a file or directory: %s', EscapedPath($dataPath);
			unless (unlink($dataPath)) {
				Warn '%s: %s', "$!", EscapedPath($dataPath);
			}
		}
	}
}

# 保守用のスキャンデータのスリム化の wanted 関数
sub _SlimScanData4Maintain {
	my	$dataPath = $File::Find::name;
	my	$realPath = substr($dataPath, $ActionSwitch{ScanDataBasePathLen});

	if (defined $ActionSwitch{targetPrefix}) {
		$realPath = ReplaceFName($realPath);
	}

	unless ($realPath eq '') {
		if (-d $dataPath) {
			if (-d $realPath) {
				chmod $Conf{'perm-dir'}, $dataPath;
			}
			elsif (-f $realPath and $Conf{dmg} && IsDiskImageFile($realPath)) {
				my	$mountPoint;
				my	@umountArgs;
				my	$check;

				$check = defined(
					$mountPoint = tempdir('ClamdOmitScan-XXXXXXXXXX',
						CLEANUP => 1, DIR => File::Spec->tmpdir
					)
				);
				if ($check) {
					$mountPoint = realpath($mountPoint);
					$check =
						0 < scalar(
							@umountArgs = MountDiskImage(
								$realPath, $mountPoint
							)
						)
							and
						$umountArgs[0] ? 1 : 0
					;
					if ($check) {
						local	$ActionSwitch{targetPrefix} = {
							prefix		=> $realPath,
							prefixLen	=> length($realPath),
							replace		=> $mountPoint,
						};

						find(\&_SlimScanData4Maintain, $dataPath);
						$check = UnmountDiskImage(@umountArgs);
					}
					else {
						Error "Failed to mount a disk image file: %s",
							$realPath
						;
					}
					rmdir $mountPoint;
				}
				unless ($check) {
					Error "$!";
				}
				$File::Find::prune = 1;
			}
			else {
				unless (rmtree($dataPath, 0, 0)) {
					Warn '%s: %s', "$!", EscapedPath($dataPath);
				}
				$File::Find::prune = 1;
			}
		}
		elsif (-f $dataPath) {
			if (-f $realPath and !$Conf{dmg} || !IsDiskImageFile($realPath)) {
				my	%version = _LastScanClamavVersion($dataPath);

				if ($version{ClamAV}) {
					chmod $Conf{'perm-file'}, $dataPath;
				}
				else {
					if (-e $dataPath) {	# _LastScanClamavVersion で消されるかも
						unless (unlink $dataPath) {
							Warn '%s: %s', "$!", EscapedPath($dataPath);
						}
					}
				}
			}
			else {
				unless (unlink($dataPath)) {
					Warn '%s: %s', "$!", EscapedPath($dataPath);
				}
			}
		}
		else {
			Warn 'This is not a file or directory: %s', EscapedPath($dataPath);
			unless (unlink($dataPath)) {
				Warn '%s: %s', "$!", EscapedPath($dataPath);
			}
		}
	}
}

# スキャンデータのスリム化
# @param $handle スキャンデータのハンドル
# @param $dir スリム化するディレクトリのパス
# @param $wanted wanted 関数
#	省略可能
# @return 成功/失敗
#	undef が返された場合はオープンされていないことを表す。
sub SlimScanData {
	my	($handle, $dir, $wanted) = @_;
	my	$scanDataDir;

	return undef	unless ($handle->{fileHandle});

	if (defined $dir) {
		$dir = realpath($dir);
		if ($dir eq '/') {
			$scanDataDir = $handle->{basePath};
		}
		else {
			$scanDataDir = File::Spec->catfile(
				$handle->{basePath}, substr($dir, 1)
			);
		}
	}
	else {
		$scanDataDir = $handle->{basePath};
	}

	if (-e $scanDataDir) {	# ファイルの可能性もある
		$wanted = \&_SlimScanData	unless(defined $wanted);
		find($wanted, $scanDataDir);
	}
	return 1;
}

# 最終スキャン日時
# @param $handle スキャンデータのハンドル
# @param $path ファイルのパス
# @return 最終スキャン日時のエポックタイム
#	オープンされていないときや記録がないときは undef が返される。
sub LastScanedTime {
	my	($handle, $path) = @_;

	return undef	unless ($handle->{fileHandle});

	$path = ReplaceFName($path);
	$path = File::Spec->catfile($handle->{basePath}, $path);
	return undef	unless (-e $path);

	return (stat($path))[9];
}

# 最終スキャン ClamAV バージョン
# @param $path スキャンデータのパス
# @return 次の形式のハッシュ
#	(
#		ClamAV	=> ClamAV のバージョン,
#		db	=> ウィルスデータベースのバージョン,
#		dbBuildDateTime	=> ウィルスデータベース構築日時,
#	)
#	daily.cvd 更新日時はエポックタイム
#	失敗したときは各要素が undef になる。
sub _LastScanClamavVersion {
	my	($path) = @_;
	my	%result = (ClamAV => undef, db => undef, dbBuildDateTime => undef);
	my	$fh;
	my	$line;

	unless (open $fh, '<', $path) {
		Warn '%s: %s', "$!", EscapedPath($path);
		if (-f $path) {
			unless (unlink $path) {
				Warn '%s: %s', "$!", EscapedPath($path);
			}
		}
		elsif (-d $path) {
			unless (rmtree($path, 0, 0)) {
				Warn '%s: %s', "$!", EscapedPath($path);
			}
		}
		return %result;
	}
	$line = <$fh>;
	close $fh;
	chomp $line;

	%result = ParseVersion($line);
	unless (defined $result{ClamAV}) {
		Warn 'Invalid version format: %s: %s', EscapedPath($path), $line;
		if (-f $path) {
			unless (unlink $path) {
				Warn '%s: %s', "$!", EscapedPath($path);
			}
		}
		elsif (-d $path) {
			unless (rmtree($path, 0, 0)) {
				Warn '%s: %s', "$!", EscapedPath($path);
			}
		}
	}

	return %result;
}

# 最終スキャン ClamAV バージョン
# @param $handle スキャンデータのハンドル
# @param $path ファイルのパス
# @return 次の形式のハッシュ
#	(
#		ClamAV	=> ClamAV のバージョン,
#		db	=> ウィルスデータベースのバージョン,
#		dbBuildDateTime	=> ウィルスデータベース構築日時,
#	)
#	daily.cvd 更新日時はエポックタイム
#	失敗したときは各要素が undef になる。
sub LastScanClamavVersion {
	my	($handle, $path) = @_;

	unless ($handle->{fileHandle}) {
		return (ClamAV => undef, db => undef, dbBuildDateTime => undef);
	}

	$path = File::Spec->catfile($handle->{basePath}, $path);
	unless (-f $path) {
		return (ClamAV => undef, db => undef, dbBuildDateTime => undef);
	}
	return _LastScanClamavVersion($path);
}


# 最終スキャン日時記録
# @param $handle スキャンデータのハンドル
# @param $path ファイルのパス
# @param $time 最終スキャン日時
# @return 成功/失敗
#	undef が返された場合はオープンされていないことを表す。
sub AddScanData {
	my	($handle, $path, $time) = @_;
	my	$scanData;
	my	$ret = 1;
	my	$dir;

	return undef	unless ($handle->{fileHandle});

	$scanData = File::Spec->catfile($handle->{basePath}, $path);
	$dir = dirname($scanData);
	unless (-e $dir) {
		my	@created =  mkpath([$dir], 0, $Conf{'perm-dir'});

		unless (0 < scalar(@created)) {
			Warn 'Can\'t create a directory: %s: %s', "$!", EscapedPath($dir);
			$ret = 0;
		}
		foreach (@created) {
			chmod $Conf{'perm-dir'}, $_;
		}
	}
	if ($ret) {
		# ファイル $scanData を作成する
		my	$fh;

		if (open $fh, '>', $scanData) {
			print $fh $ActionSwitch{ClamavVersion}->{string};
			close $fh;
			chmod $Conf{'perm-file'}, $scanData;
		}
		else {
			Warn '%s: %s', "$!", EscapedPath($scanData);
			$ret = 0;
		}
	}

	return $ret;
}

# 最終スキャン日時更新
#	記録がなければ作成される。
# @param $handle スキャンデータのハンドル
# @param $path ファイルのパス
# @return 成功/失敗
#	undef が返された場合はオープンされていないことを表す。
sub UpdateScanData {
	my	($handle, $path) = @_;
	my	$scanData;
	my	$ret = 1;

	return undef	unless ($handle->{fileHandle});

	$scanData = File::Spec->catfile($handle->{basePath}, $path);
	if (-e $scanData) {
		if (-f $scanData) {
			my	$fh;
			my	$oldUmask = umask;

			# ファイルの更新日時を更新する操作
			umask ($Conf{'perm-file'} ^ 07777);
			if (open $fh, '>', $scanData) {
				print $fh $ActionSwitch{ClamavVersion}->{string};
				close $fh;
			}
			else {
				Warn '%s: %s', "$!", EscapedPath($scanData);
				$ret = 0;
			}
			umask $oldUmask;
		}
		else {
			Warn 'Is not a file: %s', EscapedPath($scanData);
			$ret = 0;
		}
	}
	else {
		$ret = AddScanData($handle, $path);
	}

	return $ret;
}


#--- 初期化 ----
# Darwin のファイルシステムの情報を更新する
sub _UpdateFileSsystemInfo4darwin {
	my	$fh;

	Debug 'Get filesystem informations...';
	if (open $fh, '-|', qw(/usr/sbin/disktool -l)) {
		$ActionSwitch{filesystem} = [];
		while (my $line = <$fh>) {
			chomp $line;
			unless (
				$line =~ m/^\*\*\*Disk Appeared \((?#
					)'.*',(?#
					)Mountpoint = '(.*)', (?#
					)fsType = '(.*)', (?#
					)volName = '.*'\)$/
			) {
				Warn 'Unknown disktool output format: %s', $line;
				next;
			}
			my	($mp, $ft) = ($1, $2);

			next	if ($mp eq '');
			Debug "\t%s: %s", $ft, $mp;
			push @{$ActionSwitch{filesystem}}, {
				mountPoint	=> $mp,
				type		=> $ft
			};
		}
		close $fh;
		Debug 'done.';
	}
	else {
		Error '/usr/sbin/disktool -l: %s', "$!"; 
	}
}

# ファイルシステムの情報を更新する
sub UpdateFileSystemInfo {
	my	$fs = new Sys::Filesystem;

	Debug 'Get filesystem informations...';
	$ActionSwitch{filesystem} = [
		map {
			Debug "\t%s: %s", $fs->type($_), $fs->mount_point($_);
			{
				mountPoint	=> $fs->mount_point($_),
				type		=> $fs->type($_),
			};
		} $fs->mounted_filesystems()
	];
	Debug 'done.';
}

if ($^O eq 'darwin') {
	*UpdateFileSystemInfo = \*_UpdateFileSsystemInfo4darwin;
}

# 初期化
# @return 成功/失敗
sub Initialize {
	unless ($Summary{'time'}->{start} = Time::HiRes::time()) {
		Error 'Can not get hight resolution time.';
		return 0;
	}

	unless (InitializeOutputStream()) {
		return 0;
	}
	Debug 'Output streams initilized.';

	unless (InitializeWithClamdConf()) {
		return 0;
	}
	Debug 'Configured with clamd.conf.';

	unless (InitializeScanType()) {
		return 0;
	}
	Debug 'Scan type: %s', $ActionSwitch{scanType};

	unless ($ActionSwitch{ClamavVersion}->{string} = ClamavVersion()) {
		return 0;
	}
	Debug 'ClamAV version: %s', $ActionSwitch{ClamavVersion}->{string};

	$ActionSwitch{ClamavVersion}->{hash}
		= {ParseVersion($ActionSwitch{ClamavVersion}->{string})};
	unless (defined $ActionSwitch{ClamavVersion}->{hash}->{ClamAV}) {
		Error 'Invalid VERSION response from clamd: %s',
			$ActionSwitch{ClamavVersion}->{string}
		;
		return 0;
	}

	if ($Conf{rsrc}) {
		UpdateFileSystemInfo();
	}

	unless (
		$ActionSwitch{ScanDataHandle} = OpenScanData($Conf{data}, $Conf{lock})
	) {
		return 0;
	}
	Debug 'Scan data opened.';
	push @{$Conf{'exclude-dir'}}, qr|^\Q$Conf{data}\E|;

	unless ($Summary{'time'}->{scan}->{start} = Time::HiRes::time()) {
		Error 'Can not get hight resolution time.';
		return 0;
	}

	return 1;
}

# 出力ストリームの初期化
# @return 成功/失敗
sub InitializeOutputStream {
	if ($Conf{stdout}) {
		push @{$ActionSwitch{outputStream}}, \*STDOUT;
	}
	else {
		push @{$ActionSwitch{outputStream}}, \*STDERR;
	}

	if ($Conf{log}) {
		my	$fh;

		if (open $fh, '>>', $Conf{log}) {
			push @{$ActionSwitch{outputStream}}, $fh;
		}
		else {
			Error "%s: %s", "$!", EscapedPath($Conf{log});
			return 0;
		}
	}

	my	$orgSelected = select();

	foreach (@{$ActionSwitch{outputStream}}) {
		select $_;
		$| = 1;
	}
	select $orgSelected;

	return 1;
}

# clamd.conf からの初期化
#	次を決定する。
#		$Conf{host}, $Conf{port}, $Conf{'stream-max-length'}
sub InitializeWithClamdConf {
	my	$fh;
	my	$confFile = $Conf{'config-file'};

	unless ($confFile) {
		$confFile = "$Prefix/etc/clamd.conf";
	}
	if (open $fh, '<', $confFile) {
		my	%clamdConf = ();

		while (my $line = <$fh>) {
			chomp $line;
			$line =~ s/#.*//;
			$line =~ s/^\s+//;
			$line =~ s/\s+$//;
			next	if ($line eq '');

			my	($directive, $value)= split /\s+/, $line, 2;

			if (
				$directive eq 'TCPSocket'
					||
				$directive eq 'TCPAddr'
					||
				$directive eq 'LocalSocket'
					||
				$directive eq 'StreamMaxLength'
					||
				$directive eq 'MaxDirectoryRecursion'
			) {
				$clamdConf{$directive} = $value;
			}
		}
		close $fh;

		# 解釈が必要な値の解釈
		if (defined $clamdConf{StreamMaxLength}) {
			if ($clamdConf{StreamMaxLength} =~ m/^(\d+)(\D+)$/) {
				my	($value, $unit) = ($1, $2);

				if ($unit eq 'K' or $unit eq 'k') {
					$clamdConf{StreamMaxLength} = $value * 1024;
				}
				elsif ($unit eq 'M' or $unit eq 'm') {
					$clamdConf{StreamMaxLength} = $value * 1024 * 1024;
				}
				elsif ($unit eq '') {
					$clamdConf{StreamMaxLength} = $value;
				}
				else {
					Warn 'Unknow unit for StreamMaxLength: %s', $unit;
					$clamdConf{StreamMaxLength} = undef;
				}
			}
		}

		# clamd.conf デフォルト値を設定する
		unless (defined $clamdConf{StreamMaxLength}) {
			$clamdConf{StreamMaxLength} = 10 * 1024 * 1024;	# 10MB
		}
		unless (defined $clamdConf{MaxDirectoryRecursion}) {
			$clamdConf{MaxDirectoryRecursion} = 15;
		}

		# clamd.conf の設定を反映する
		if (defined $clamdConf{LocalSocket}) {
			unless (defined $Conf{'socket'}) {
				$Conf{'socket'} = $clamdConf{LocalSocket};
			}
		}
		if (defined $clamdConf{TCPSocket}) {
			unless (defined $Conf{port}) {
				$Conf{port} = $clamdConf{TCPSocket};
			}
		}
		if (defined $clamdConf{TCPAddr}) {
			unless (defined $Conf{host}) {
				$Conf{host} = $clamdConf{TCPAddr};
			}
		}
		if (defined $clamdConf{StreamMaxLength}) {
			unless (defined $Conf{'stream-max-length'}) {
				$Conf{'stream-max-length'} = $clamdConf{StreamMaxLength};
			}
		}
		if (defined $clamdConf{MaxDirectoryRecursion}) {
			unless (defined $Conf{'max-dir-recursion'}) {
				$Conf{'max-dir-recursion'} = $clamdConf{MaxDirectoryRecursion};
			}
		}
	}
	elsif (defined $Conf{'config-file'}) {
		Warn '%s: %s', "$!", EscapedPath($Conf{'config-file'});
	}

	# デフォルト値を設定する
	#	$Conf{'socket'} はここでは設定しない。
	#	InitilizeScanType で決定する。
	unless ($Conf{'stream-max-length'}) {
		$Conf{'stream-max-length'} = 10 * 1024 * 1024;	# 10MB
	}
	unless ($Conf{'max-dir-recursion'}) {
		$Conf{'max-dir-recursion'} = 15;
	}
}

# スキャンタイプの初期化
#	InitializeWithClamdConf よりも後に実行しなければならない
# @return 成功/失敗
sub InitializeScanType {
	# UNIX ドメインソケット
	if (defined($Conf{'socket'}) or !defined($Conf{port})) {
		unless (defined $Conf{'socket'}) {
			$Conf{'socket'} = '/tmp/clamd.socket';
		}
		$ActionSwitch{scanType} = 'unix';
		$ActionSwitch{ScanFunc} = $Conf{rsrc}
			? \&ScanFileWithRsrc
			: \&ScanFile
		;
		if ($Conf{multiscan}) {
			$ActionSwitch{ClamdScanCommand} = 'zMULTISCAN';
		}
		elsif ($Conf{contscan}) {
			$ActionSwitch{ClamdScanCommand} = 'zCONTSCAN';
		}
		else {
			$ActionSwitch{ClamdScanCommand} = 'zSCAN';
		}
		# UNIX ドメインソケットを使用しても STREAM をするときはデータ送信に
		# INET ソケットを使用しなければならない。だから host を設定する。
		$Conf{host} = '127.0.0.1'	unless (defined $Conf{host});
	}
	# INET ソケット
	else {
		$Conf{host} = '127.0.0.1'	unless (defined $Conf{host});
		$ActionSwitch{scanType} = 'inet';
		$ActionSwitch{ScanFunc} = $Conf{rsrc}
			? \&ScanStreamWithRsrc
			: \&ScanStream
		;
	}

	return 1;
}

# 移動/コピーディレクトリのセット
# @param $name オプション名
# @param $value オプション値
# @return 成功/失敗
sub SetMoveDir {
	my	($name, $value) = @_;

	unless (-e $value) {
		Error 'No such directory: %s', EscapedPath($value);
		return 0;
	}
	unless (-d $value) {
		Error 'Not a directory: %s', EscapedPath($value);
		return 0;
	}
	unless (-w $value and -x $value) {
		Error 'Permission denied: %s', EscapedPath($value);
		return 0;
	}

	$Conf{$name} = $value;
	return 1;
}

# HFS 判定
# @param $path	パス
# @return HFS ファイルシステム上のパスかどうか
sub IsHfsPath {
	my	($path) = @_;
	my	$rootDir = File::Spec->rootdir;

	for (; $path ne $rootDir; $path = dirname($path)) {
		foreach my $fs (@{$ActionSwitch{filesystem}}) {
			if ($path eq $fs->{mountPoint}) {
				return $fs->{type} eq 'hfs';
			}
		}
	}
	foreach my $fs (@{$ActionSwitch{filesystem}}) {
		if ($rootDir eq $fs->{mountPoint}) {
			return $fs->{type} eq 'hfs';
		}
	}

	return 0;
}

#--- 終了処理 ----
# 終了処理
sub Terminate {
	$Summary{'time'}->{scan}->{end} = Time::HiRes::time();
	foreach (keys %{$ActionSwitch{socks}}) {
		CloseClamd($_);
	}
	ReportSummary();
	CloseScanData($ActionSwitch{ScanDataHandle});
	$Summary{'time'}->{end} = Time::HiRes::time();
	CloseOutputStreams();
}

# サマリ出力
sub ReportSummary {
	return	unless ($Conf{summary});

	unless ($Summary{'time'}->{scan}->{start}) {
		return;
	}

	my	$startAt = localtime($Summary{'time'}->{start});
	my	$period = $Conf{period};
	my	$scanTime =
			$Summary{'time'}->{scan}->{end}
				-
			$Summary{'time'}->{scan}->{start}
	;
	my	$scanTimeM = int($scanTime / 60);
	my	$scanTimeS = int($scanTime - ($scanTimeM * 60));
	my	$scanedData = $Summary{count}->{data}->{scaned};
	my	$scanedDataUnit = 'B';
	my	$checkedData = $Summary{count}->{data}->{checked};
	my	$checkedDataUnit = 'B';

	if (365*24*60*60 <= $period) {
		$period = sprintf '%d sec (%.1f y)',
			$period, $period / (365*24*60*60)
		;
	}
	elsif (30*24*60*60 <= $period) {
		$period = sprintf '%d sec (%.1f M)',
			$period, $period / (30*24*60*60)
		;
	}
	elsif (7*24*60*60 <= $period) {
		$period = sprintf '%d sec (%.1f w)',
			$period, $period / (7*24*60*60)
		;
	}
	elsif (24*60*60 <= $period) {
		$period = sprintf '%d sec (%.1f d)',
			$period, $period / (24*60*60)
		;
	}
	elsif (60*60 <= $period) {
		$period = sprintf '%d sec (%.1f h)',
			$period, $period / (60*60)
		;
	}
	elsif (60 <= $period) {
		$period = sprintf '%d sec (%.1f m)',
			$period, $period / (60)
		;
	}

	foreach my $unit (qw(KB MB GB TB)) {
		last	unless (1024 < $scanedData);
		$scanedData = $scanedData / 1024;
		$scanedDataUnit = $unit;
	}

	foreach my $unit (qw(KB MB GB TB)) {
		last	unless (1024 < $checkedData);
		$checkedData = $checkedData / 1024;
		$checkedDataUnit = $unit;
	}

	Output("
----------- SCAN SUMMARY -----------
Start at: %s
Server: %s
Engine version: %s
Inspection period: %s
Scanning data: %s
Scanned directories: %d
Scanned files: %d
Checked files: %d
Infected files: %d
Data scanned: %.2f %s
Data checked: %.2f %s
Time: %.3f sec (%d m %d s)
",
		$startAt,
		$ActionSwitch{scanType} eq 'unix' ?
			$Conf{'socket'} : "$Conf{host}:$Conf{port}",
		$ActionSwitch{ClamavVersion}->{string},
		$period,
		$Conf{data},
		$Summary{count}->{directories},
		$Summary{count}->{files}->{scaned},
		$Summary{count}->{files}->{checked},
		$Summary{count}->{files}->{infected},
		$scanedData, $scanedDataUnit,
		$checkedData, $checkedDataUnit,
		$scanTime, $scanTimeM, $scanTimeS,
	);
}

# 出力ストリームのクローズ
sub CloseOutputStreams {
	foreach (reverse @{$ActionSwitch{outputStream}}) {
		close $_;
	}
}

#--- スキャン対象判定 ----
# チェック期間ファイル判定
# @param $path ファイルのパス
# @return スキャン必要かどうか
sub DoesNeedScan {
	my	($path) = @_;
	my	$mtime;

	# ファイルの更新日時を、mtime と ctime のうち新しい方とする。
	{
		my	@stat =  stat $path;

		$ActionSwitch{statCache} = [@stat];
		if ($Conf{rsrc} and $ActionSwitch{onHfsDir}) {
			$ActionSwitch{statCache}->[7] += (stat "$path/rsrc")[7];
		}
		$mtime = $stat[9] <= $stat[10] ? $stat[10] : $stat[9];
	}

	# 最終スキャン日時より後に更新されているならスキャンが必要。
	{
		my	$lastScanedTime = LastScanedTime(
			$ActionSwitch{ScanDataHandle}, $path
		);

		return 1	if (
			!defined($lastScanedTime) or $lastScanedTime <= $mtime
		);
	}

	# 同じバージョンで二回以上スキャンする必要はない。
	{
		my	%version = LastScanClamavVersion(
			$ActionSwitch{ScanDataHandle}, $path
		);

		if (
			defined($version{ClamAV})
				and
			defined($ActionSwitch{ClamavVersion}->{hash}->{ClamAV})
				and
			$version{ClamAV} eq $ActionSwitch{ClamavVersion}->{hash}->{ClamAV}
				and
			$version{db} eq $ActionSwitch{ClamavVersion}->{hash}->{db}
		) {
			Info 'Already scaned with current version: %s', EscapedPath($path);
			return 0;
		}
	}

	# 最終スキャン時のウィルスデータベースの構築日時が
	# ファイルの更新日時よりも充分後ならスキャンする必要はない。
	if (
		$mtime + $Conf{period}
			<=
		$ActionSwitch{ClamavVersion}->{hash}->{dbBuildDateTime}
	) {
		Info 'Inspection period is over: %s', EscapedPath($path);
		return 0;
	}

	return 1;
}

# スキャン対象ディレクトリ判定
# @param $path ディレクトリのパス
# @return スキャン対象かどうか
sub IsScanTargetDirectory {
	my	($path) = @_;

	$path = ReplaceFName($path);
	!IsExcludeDir($path) and IsIncludeDir($path);
}

# スキャン対象ファイル判定
# @param $path ファイルのパス
# @return スキャン対象かどうか
sub IsScanTargetFile {
	my	($path) = @_;

	$path = ReplaceFName($path);
	!IsExcludeFile($path) and IsIncludeFile($path);
}

# スキャン対象除外ディレクトリ判定
# @param $path ディレクトリのパス
# @return スキャン対象かどうか
sub IsExcludeDir {
	my	($path) = @_;

	return grep { $path =~ m/$_/ } @{$Conf{'exclude-dir'}};
}

# スキャン対象ディレクトリ判定
# @param $path ディレクトリのパス
# @return スキャン対象かどうか
sub IsIncludeDir {
	my	($path) = @_;

	scalar(@{$Conf{'include-dir'}}) == 0
		or
	grep { $path =~ m/$_/ } @{$Conf{'include-dir'}}
}

# スキャン対象除外ファイル判定
# @param $path ディレクトリのパス
# @return スキャン対象かどうか
sub IsExcludeFile {
	my	($path) = @_;

	return grep { $path =~ m/$_/ } @{$Conf{exclude}};
}

# スキャン対象ファイル判定
# @param $path ディレクトリのパス
# @return スキャン対象かどうか
sub IsIncludeFile {
	my	($path) = @_;

	scalar(@{$Conf{include}}) == 0
		or
	grep { $path =~ m/$_/ } @{$Conf{include}}
}

#--- スキャンラッパー ----
# ファイルまたはディレクトリのスキャン
# @param $target スキャン対象のパス
sub Scan {
	my	($target) = @_;

	$target = realpath($target);
	Debug 'Scan target: %s', EscapedPath($target);

	unless (SlimScanData($ActionSwitch{ScanDataHandle}, $target)) {
		return 0;
	}

	if (-f $target) {
		my	$dir = dirname($target);

		if ($Conf{rsrc}) {
			$ActionSwitch{onHfsDir} = IsHfsPath($target);
		}

		if (!IsScanTargetDirectory($dir)) {
			ReportExcluded($target);
			$target = undef;
		}
		elsif (!IsScanTargetFile($target)) {
			ReportExcluded($target);
			$target = undef;
		}
		elsif (
			!$Conf{dmg} || !IsDiskImageFile($target) and !DoesNeedScan($target)
		) {
			ReportChecked($target);
			$target = undef;
		}
		elsif (
			-z $target
				and
			(
				!$Conf{rsrc}
					or
				!IsHfsPath($target)
					or
				-z "$target/rsrc"
			)
		) {
			ReportOkResult($target);
		}
	}
	elsif (!-d $target) {
		Warn "Is not a file or directory: %s", EscapedPath($target);
		$target = undef;
	}
	elsif (!$Conf{recursive}) {
		$target = undef;
	}

	if (defined $target) {
		$ActionSwitch{dirDepth} = 0;
		find(
			{
				wanted		=> \&_Scan,
				preprocess	=> $Conf{rsrc}
					? sub {
						my	@list = @_;
	
						$ActionSwitch{onHfsDir} = IsHfsPath($File::Find::dir);
						$ActionSwitch{dirDepth}++;
						return @list;
					}
					: sub {
						my	@list = @_;
	
						$ActionSwitch{dirDepth}++;
						return @list;
					},
				postprocess => sub {
					$ActionSwitch{dirDepth}--;
				},
			},
			$target
		);
	}
}

# スキャンの wanted 関数
sub _Scan {
	my	$path = $File::Find::name;

	if (-d $path) {
		if (
			!IsScanTargetDirectory($path)
				or
			$Conf{'max-dir-recursion'} < $ActionSwitch{dirDepth}
		) {
			$File::Find::prune = 1;
			Debug '%s: Excluded', EscapedPath($path);
		}
		else {
			$Summary{count}->{directories}++;
		}
	}
	elsif (-f $path) {
		if (!IsScanTargetFile($path)) {
			ReportExcluded($path);
		}
		elsif (
			!$Conf{dmg} || !IsDiskImageFile($path) and !DoesNeedScan($path)
		) {
			ReportChecked($path);
		}
		elsif (
			-z $path
				and
			(
				!$Conf{rsrc}
					or
				!$ActionSwitch{onHfsDir}
					or
				-z "$path/rsrc"
			)
		) {
			ReportOkResult($path);
		}
		else {
			my	$timeout;
			my	%result;

			if ($Conf{dmg} && IsDiskImageFile($path)) {
				$timeout = $Conf{timeout};
			}
			else {
				$timeout = GetScanTimeout($path);
			}
			Debug 'timeout: %.3f s: %s', $timeout, EscapedPath($path);
			eval {
				local	$SIG{ALRM};

				$SIG{ALRM} = sub { die 'clamd timeout' };
				alarm $timeout;
				%result = &{$ActionSwitch{ScanFunc}}($path);
				alarm 0;
			};
			if ($@) {
				alarm 0;
				chomp $@;
				if ($@ =~ m/^clamd timeout/) {
					$ActionSwitch{timeoutFreq}++;
					Warn 'Scan timeout: %s', EscapedPath($path);
					if ($Conf{'timeout-command'}) {
						system $Conf{'timeout-command'};
					}
				}
				else {
					Warn '%s: %s', $@, EscapedPath($path);
				}

				# 少し待ってからクローズする。
				# timeout 設定が短過ぎると接続が確立する前にここに来るから。
				sleep(1);
				foreach (keys %{$ActionSwitch{socks}}) {
					CloseClamd($_);
				}

				if (
					0 <= $Conf{'limit-timeout'}
						and
					$Conf{'limit-timeout'} < $ActionSwitch{timeoutFreq}
				) {
					die 'Over frequence of continual clamd timeout.';
				}
			}
			else {
				$ActionSwitch{timeoutFreq} = 0;
			}
			ReportResult($path, %result);
		}
	}
}

# スキャンタイムアウト
# @param $path ファイルのパス
# @return 秒数
sub GetScanTimeout {
	my	($path) = @_;

	return
		$ActionSwitch{statCache}->[7] / 1024 / 1024
			*
		$Conf{'scan-timeout'}
			+
		$Conf{timeout}
	;
}

#--- clamd コミュニケーション ----
# ファイルをマウントしてスキャン
# @param $path	スキャン対象のファイル
sub MountScan {
	my	($path) = @_;
	my	%result;
	my	$mountPoint;
	my	@umountArgs;
	my	$check;

	$check = defined(
		$mountPoint = tempdir('ClamdOmitScan-XXXXXXXXXX',
			CLEANUP => 1, DIR => File::Spec->tmpdir
		)
	);
	if ($check) {
		$mountPoint = realpath($mountPoint);
		$check =
			(
				0 < scalar(@umountArgs = MountDiskImage($path, $mountPoint))
					and
				defined($umountArgs[0])
			) ? 1 : 0
		;
		if ($check) {
			my	$resultStr = '';
			my	$realPath = ReplaceFName($path);

			{
				local	$Conf{recursive} = 1;
				local	$ActionSwitch{onHfsDir};
				local	$ActionSwitch{targetPrefix};

				$ActionSwitch{targetPrefix} = {
					prefix		=> $mountPoint,
					prefixLen	=> length($mountPoint),
					replace		=> $realPath,
				};

				find(
					{
						wanted		=> \&_Scan,
						preprocess	=> $Conf{rsrc}
							? sub {
								my	@list = @_;
			
								$ActionSwitch{onHfsDir} = IsHfsPath(
									$File::Find::dir
								);
								$ActionSwitch{dirDepth}++;
								return @list;
							}
							: sub {
								my	@list = @_;
			
								$ActionSwitch{dirDepth}++;
								return @list;
							},
						postprocess => sub {
							$ActionSwitch{dirDepth}--;
						},
					},
					$mountPoint
				);
				$check = UnmountDiskImage(@umountArgs);

				if ($ActionSwitch{targetPrefix}->{_infected}) {
					MoveInfected($path);
				}
			}
		}
		else {
			push @{$result{ERROR}}, [$path, 'Failed to mount.'];
		}
		rmdir $mountPoint;
	}
	unless ($check) {
		push @{$result{ERROR}}, [$path, $!];
	}
}

# ファイルスキャン
# @param $path ファイルのパス
# @return 次の形式のハッシュ
#	(
#		OK	=> [ファイル名, ...],
#		FOUND	=> [[ファイル名, ウィルス名], ...],
#		ERROR	=> [[ファイル名, エラーメッセージ], ...],
#		UNKNOWN	=> [[ファイル名, メッセージ], ...],
#	)
sub ScanFile {
	my	($path) = @_;
	my	%result = (OK => [], FOUND => [], ERROR => [], UNKNOWN => []);

	if ($Conf{dmg} and IsDiskImageFile($path)) {
		MountScan($path);
	}
	else {
		my	$conn;
		my	$command;
		my	$result = undef;

		$conn = OpenClamdWithUnixDomainSocket();
		if ($conn) {
			$command = sprintf(
				"%s %s\000", $ActionSwitch{ClamdScanCommand}, $path
			);
			$result = SendClamdCommand($conn, $command);
			CloseClamd($conn);
		}
		if (defined $result) {
			%result = ParseScanResult($result);
			if (scalar(@{$result{UNKNOWN}})) {
				foreach (@{$result{UNKNOWN}}) {
					$_->[0] = $path;
				}
			}
		}
		else {
			push @{$result{ERROR}}, [$path, 'clamd communication error.'];
		}
	}

	return %result;
}

# リソースフォークを含むファイルスキャン
# @param $path ファイルのパス
# @return 次の形式のハッシュ
#	(
#		OK	=> [ファイル名, ...],
#		FOUND	=> [[ファイル名, ウィルス名], ...],
#		ERROR	=> [[ファイル名, エラーメッセージ], ...],
#		UNKNOWN	=> [[ファイル名, メッセージ], ...],
#	)
sub ScanFileWithRsrc {
	unless ($ActionSwitch{onHfsDir}) {
		return ScanFile(@_);
	}

	my	($path) = @_;
	my	%result = (OK => [], FOUND => [], ERROR => [], UNKNOWN => []);

	if (-z $path) {
		push @{$result{OK}}, $path;
	}
	else {
		my	%forkResult = ScanFile($path);

		foreach my $key (keys %result) {
			push @{$result{$key}}, @{$forkResult{$key}};
		}
	}

	if (-z "$path/rsrc") {
		push @{$result{OK}}, $path;
	}
	else {
		my	%forkResult = ScanFile("$path/rsrc");

		ReplacePathInResult($path, \%forkResult);
		foreach my $key (keys %result) {
			push @{$result{$key}}, @{$forkResult{$key}};
		}
	}

	if (scalar(@{$result{OK}}) == 2) {
		pop @{$result{OK}};
	}
	else {
		$result{OK} = [];
	}

	return %result;
}

# ファイルのストリームスキャン
# @param $path ファイルのパス
# @return 次の形式のハッシュ
#	(
#		OK	=> [ファイル名, ...],
#		FOUND	=> [[ファイル名, ウィルス名], ...],
#		ERROR	=> [[ファイル名, エラーメッセージ], ...],
#		UNKNOWN	=> [[ファイル名, メッセージ], ...],
#	)
sub ScanStream {
	my	($path) = @_;
	my	$conn;
	my	%result = (OK => [], FOUND => [], ERROR => [], UNKNOWN => []);

	if ($Conf{dmg} and IsDiskImageFile($path)) {
		MountScan($path);
	}
	elsif ($Conf{'stream-max-length'} < $ActionSwitch{statCache}->[7]) {
		push @{$result{ERROR}}, [$path, 'Too large.'];
	}
	else {
		$conn = $ActionSwitch{scanType} eq 'unix'
			? OpenClamdWithUnixDomainSocket()
			: OpenClamdWithInetSocket()
		;
		if ($conn) {
			my	$fh;

			Debug 'Connected to clamd.';
			if (open $fh, '<', $path) {
				eval {
					my	$sentLen;
					my	$buffer;
					my	$bufsiz;
	
					$sentLen = $conn->syswrite("zINSTREAM\000", 10);
					unless ($sentLen == 10) { die; }
					while ($bufsiz = read($fh, $buffer, $Conf{bufsiz})) {
						$sentLen = $conn->syswrite(
							NetworkByteOrderUnsingedDouble($bufsiz) . $buffer,
							4 + $bufsiz
						);
						unless ($sentLen == 4 + $bufsiz) { die; }
					}
					$sentLen = $conn->syswrite("\000\000\000\000", 4);
					unless ($sentLen == 4) { die; }
				};
				if ($@) {
					push @{$result{ERROR}}, [$path, 'clamd communication error.'];
				}
				close $fh;
			}
			else {
				push @{$result{ERROR}}, [$path, "$!"];
			}

			if ($conn->connected()) {
				my	$result = '';
				my	%returnedResult;
		
				while (my $line = $conn->getline()) {
					$result .= $line;
				}
				%returnedResult = ParseScanResult($result);
				ReplacePathInResult($path, \%returnedResult);
				while (my ($key, $value) = each %returnedResult) {
					if (defined $result{$key}) {
						push @{$result{$key}}, @{$value};
					}
					else {
						$result{$key} = $value;
					}
				}

				CloseClamd($conn);
			}
		}
		else {
			push @{$result{ERROR}}, [$path, "$!"];
		}
	}

	return %result;
}

# リソースフォークを含むファイルスキャン
# @param $path ファイルのパス
# @return 次の形式のハッシュ
#	(
#		OK	=> [ファイル名, ...],
#		FOUND	=> [[ファイル名, ウィルス名], ...],
#		ERROR	=> [[ファイル名, エラーメッセージ], ...],
#		UNKNOWN	=> [[ファイル名, メッセージ], ...],
#	)
sub ScanStreamWithRsrc {
	unless ($ActionSwitch{onHfsDir}) {
		return ScanStream(@_);
	}

	my	($path) = @_;
	my	%result = (OK => [], FOUND => [], ERROR => [], UNKNOWN => []);

	if (-z $path) {
		push @{$result{OK}}, $path;
	}
	else {
		my	%forkResult = ScanStream($path);

		foreach my $key (keys %result) {
			push @{$result{$key}}, @{$forkResult{$key}};
		}
	}

	if (-z "$path/rsrc") {
		push @{$result{OK}}, $path;
	}
	else {
		my	%forkResult = ScanStream("$path/rsrc");

		ReplacePathInResult($path, \%forkResult);
		foreach my $key (keys %result) {
			push @{$result{$key}}, @{$forkResult{$key}};
		}
	}

	if (scalar(@{$result{OK}}) == 2) {
		pop @{$result{OK}};
	}
	else {
		$result{OK} = [];
	}

	return %result;
}

# スキャン結果のファイルパス入れ替え
# @param $path パス名
# @param $result スキャン結果のハッシュへのリファイレンス
#	[
#		OK	=> [ファイル名, ...],
#		FOUND	=> [[ファイル名, ウィルス名], ...],
#		ERROR	=> [[ファイル名, エラーメッセージ], ...],
#		UNKNOWN	=> [[ファイル名, メッセージ], ...],
#	]
sub ReplacePathInResult {
	my	($path, $result) = @_;
	my	$i;

	for ($i = 0; $i < scalar(@{$result->{OK}}); $i++) {
		$result->{OK}->[$i] = $path;
	}
	for ($i = 0; $i < scalar(@{$result->{FOUND}}); $i++) {
		$result->{FOUND}->[$i]->[0] = $path;
	}
	for ($i = 0; $i < scalar(@{$result->{ERROR}}); $i++) {
		$result->{ERROR}->[$i]->[0] = $path;
	}
	for ($i = 0; $i < scalar(@{$result->{UNKNOWN}}); $i++) {
		$result->{UNKNOWN}->[$i]->[0] = $path;
	}
}

# UNIX ドメインソケットによる clamd 接続
# @param $sockPath UNIX ドメインソケットのパス
# @return IO::Socket オブジェクト
sub OpenClamdWithUnixDomainSocket {
	my	($sockPath) = @_;
	my	$con = undef;

	$sockPath = $Conf{'socket'}	unless (defined $sockPath);
	$con = IO::Socket::UNIX->new(
		Type 		=> SOCK_STREAM,
		Peer 		=> $Conf{'socket'},
	);

	if ($con) {
		 $ActionSwitch{socks}->{$con} = undef;
	}

	return $con;
}

# INET ソケットによる clamd 接続
# @param $host サーバアドレス(省略可能)
#	省略すると $Conf{host} が使用される。
# @param $port ポート番号(省略可能)
#	省略すると $Conf{port} が使用される。
# @return IO::Socket オブジェクト
sub OpenClamdWithInetSocket {
	my	($host, $port) = @_;
	my	$con = undef;

	$host = $Conf{host}	unless (defined $host);
	$port = $Conf{port}	unless (defined $port);
	$con = IO::Socket::INET->new(
		Type		=> SOCK_STREAM,
		PeerAddr	=> $host,
		PeerPort	=> $port,
		Proto		=> 'tcp',
		Timeout		=> $Conf{timeout},
	);

	if ($con) {
		 $ActionSwitch{socks}->{$con} = undef;
	}

	return $con;
}

# clamd 切断
# @param $conn IO::Socket オブジェクト
sub CloseClamd {
	my	($conn) = @_;

	delete $ActionSwitch{$conn};

	eval {
		$conn->close();
	};
}

# clamd へのコマンド送信
# @param $conn IO::Socket オブジェクト
# @param $command コマンド
# @return clamd の応答(文字列)
#	失敗時は undef を返す。
sub SendClamdCommand {
	my	($conn, $command) = @_;
	my	$result = '';

	unless (SendData2Socket($conn, $command)) {
		return undef;
	}
	while (my $line = $conn->getline()) {
		$result .= $line;
	}

	return $result;
}

# データ送信
# @param $conn IO::Socket オブジェクト
# @param $data データ
# @param $len データ長(省略可能)
# @return 成功/失敗
sub SendData2Socket {
	my	($conn, $data, $len) = @_;
	my	$sentLen;

	return undef	unless (defined $data);
	$len = length($data)	unless (defined $len);
	$sentLen = $conn->syswrite($data, $len);
	return undef	unless (defined $sentLen);

	return $len == $sentLen;
}

# スキャン応答のパース
# @param $result スキャン応答
# @return 次の形式のハッシュ
#	(
#		OK	=> [ファイル名, ...],
#		FOUND	=> [[ファイル名, ウィルス名], ...],
#		ERROR	=> [[ファイル名, エラーメッセージ], ...],
#		UNKNOWN	=> [[ファイル名, メッセージ], ...],
#	)
sub ParseScanResult {
	my	($result) = @_;
	my	%result = (OK => [], FOUND => [], ERROR => [], UNKNOWN => []);

	foreach my $line (split /\000/, $result) {
		if ($line =~ m/^(.*): OK$/) {
			push @{$result{OK}}, $1;
		}
		elsif ($line =~ /^(.*): (\S+) FOUND$/) {
			push @{$result{FOUND}}, [$1, $2];
		}
		elsif ($line =~ /^(.*): (.*) ERROR$/) {
			push @{$result{ERROR}}, [$1, $2];
		}
		else {
			push @{$result{UNKNOWN}}, [undef, $line];
		}
	}

	return %result;
}

# ClamAV バージョン取得
# @return バージョン文字列
#	失敗時は undef が返される。
sub ClamavVersion {
	my	$conn = undef;
	my	$result = undef;

	if ($ActionSwitch{scanType} eq 'unix') {
		$conn = OpenClamdWithUnixDomainSocket();
	}
	elsif ($ActionSwitch{scanType} eq 'inet') {
		$conn = OpenClamdWithInetSocket();
	}
	else {
		if (defined $ActionSwitch{scanType}) {
			Error 'Unknown scan type is not specified: %s',
				$ActionSwitch{scanType}
			;
		}
		else {
			Error 'Scan type is not specified.';
		}
	}

	if ($conn) {
		$result = SendClamdCommand($conn, 'VERSION');
		chomp $result	if (defined $result);
		CloseClamd($conn);
	}
    else {
		if ($ActionSwitch{scanType} eq 'unix') {
			Error '%s: %s', "$!", EscapedPath($Conf{socket});
		}
		else {
			Error '%s: %s:%d', "$!", $Conf{host}, $Conf{port};
		}
    }

	return $result;
}

# バージョンのパース
# @param $version バージョン文字列
# @return 次の形式のハッシュ
#	(
#		ClamAV	=> ClamAV のバージョン,
#		db	=> ウィルスデータベースのバージョン,
#		dbBuildDateTime	=> ウィルスデータベース構築日時,
#	)
#	ウィルスデータベース構築日時はエポックタイム
#	失敗したときは各要素が undef になる。
sub ParseVersion {
	my	($version) = @_;
	my	@version;
	my	%version = (ClamAV => undef, db => undef, dbBuildDateTime => undef);

	@version = split '/', $version, 3;
	unless (scalar(@version) == 3) {
		return %version;
	}

	unless (
		$version[2]
			=~
		m/^\w{3} (\w{3}) ( \d|\d{2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/
	) {
		return %version;
	}
	{
		my	($mname, $mday, $hour, $min, $sec, $year) = (
			$1, $2, $3, $4, $5, $6
		);

		$version{dbBuildDateTime} = timelocal(
			$sec, $min, $hour, $mday,
			$MonthName2Month{$mname}, $year - 1900
		);
	}

	unless ($version[1] =~ m/^(\d+)$/) {
		return %version;
	}
	$version{db} = $1;

	# この値を成功したか否かの判定に使用するから一番最後にセットする。
	unless ($version[0] =~ m/^ClamAV (.*)$/) {
		return %version;
	}
	$version{ClamAV} = $1;

	return %version;
}

# ネットワークバイトオーダーの非負 4 バイト整数
sub NetworkByteOrderUnsingedDouble;

# ネットワークバイトオーダーの非負長整数
sub _NetworkByteOrderUnsingedLong {
	my	($int) = @_;

	return pack('N', $int);
}
# ネットワークバイトオーダーの非負短整数
sub _NetworkByteOrderUnsingedShort {
	my	($int) = @_;

	return pack('n', $int);
}
# ネットワークバイトオーダーの非負 4 バイト整数(非32ビット用)
sub _NetworkByteOrderUnsingedDouble {
	my	($int) = @_;
	my	$packed = '';
	
	for (my $count = 0; $count < 4; $count++) {
		my	$mod = $int % 256;

		$packed = pack('C', $mod) . $packed;
		$int = $int >> 8;
	}

	return $packed;
}

BEGIN {
	if (length(_NetworkByteOrderUnsingedLong(0)) == 4) {
		*NetworkByteOrderUnsingedDouble = *_NetworkByteOrderUnsingedLong;
	}
	elsif (length(_NetworkByteOrderUnsingedShort(0)) == 4) {
		*NetworkByteOrderUnsingedDouble = *_NetworkByteOrderUnsingedShort;
	}
	else {
		*NetworkByteOrderUnsingedDouble = *_NetworkByteOrderUnsingedDouble;
	}
}

#--- ファイルの移動/コピー ----
# 感染ファイルの移動またはコピー
#	--move または --copy が指定されていなければ何もせず成功を返す。
#	両方指定されている場合は --move が優先される。
#	元ファイルと移動またはコピー先のファイルとが同じ inode のときは、警告した上
#	で何もせず成功を返す。
#	同名のファイルが既にあるときはファイル名に ".NNN" という拡張子が付加される。
#	NNN は 000 から 999 までの数字列。そうした上で同名のファイルがない最初のファ
#	イル名が使用される。もし 999 まで既に使用されていたらそのファイルは上書きさ
#	れる。
# @param $path パス
# @return 成功/失敗
sub MoveInfected {
	my	($path) = @_;
	my	$movedPath;

	# ディスクイメージファイルの中のファイルのとき
	if (
		$ActionSwitch{targetPrefix}
			and
		substr($path, 0, $ActionSwitch{targetPrefix}->{prefixLen})
			eq
		$ActionSwitch{targetPrefix}->{prefix}
			and
		$path ne $ActionSwitch{targetPrefix}->{prefix}
	) {
		# CONTSCAN が指定されていなければディスクイメージファイル内のスキャンを
		# 中止する。
		$File::Find::prune = 1	unless($Conf{contscan});

		# 制御側で移動をさせるため感染フラグを付ける
		$ActionSwitch{targetPrefix}->{_infected} = 1;

		return 1;
	}

	# 移動先ファイル名の決定
	{
		my	$moveDir = undef;

		if (defined($Conf{move})) {
			$moveDir = $Conf{move};
		}
		elsif (defined($Conf{copy})) {
			$moveDir = $Conf{copy};
		}
		return 0	unless (defined($moveDir));

		# 後からパーミッションが変えられるかもしれないからチェックする。
		unless (-w $moveDir and -x $moveDir) {
			Warn "Can't %s file '%s': cannot write to '%s'",
				defined($Conf{move}) ? 'move' : 'copy',
				EscapedPath($path), EscapedPath($moveDir)
			;
			return undef;
		}

		$movedPath = File::Spec->catfile($moveDir, basename($path));
	}

	# 移動先ファイル名の重複解消
	if (-e $movedPath) {
		my	$newMovedPath = $movedPath;

		unless ((stat($movedPath))[1] != $ActionSwitch{statCache}->[1]) {
			Warn "File excluded '%s'", EscapedPath($path);
			return 1;
		}
		for (my $i = 0; $i < 1000; $i++) {
			$newMovedPath = sprintf '%s.%03d', $movedPath, $i;
			last	if (!-e $newMovedPath);
		}

		$movedPath = $newMovedPath;
	}

	# 移動
	if (!defined($Conf{move}) or !rename($path, $movedPath)) {
		# コピーであるか、あるいは rename システムコールが失敗したとき
		if (!CopyFile($path, $movedPath)) {
			Warn "Can't %s '%s' to '%s': %s",
				defined($Conf{move}) ? 'move' : 'copy',
				EscapedPath($path), EscapedPath($movedPath), "$!"
			;
			# 移動先が既存のファイルでもどうせ書き込んでしまっているから
			# 消してしまえ! それもできなきゃ諦めろ。
			unlink $movedPath;
			return undef;
		}
	}

	# ファイル属性の維持
	chmod $ActionSwitch{statCache}->[2], $movedPath;
	chown @{$ActionSwitch{statCache}}[4 .. 5], $movedPath;
	utime @{$ActionSwitch{statCache}}[8 .. 9], $movedPath;

	# 必要ならば元ファイル削除
	if (defined($Conf{move}) and -f $path) {
		unless (unlink $path) {
			Warn "Can't unlink '%s': %s", EscapedPath($path), "$!";
			return undef;
		}
	}

	ReportMoved($path, $movedPath);
	return 1;
}

# ファイルのコピー
# @param $src	元ファイルのパス
# @param $dst	先ファイルのパス
# @return 成功/失敗
sub CopyFile {
	my	($src, $dst) = @_;

	# Mac OS X の場合
	if ($^O eq 'darwin' and -f '/usr/bin/ditto' and -x '/usr/bin/ditto') {
		# リソースフォークを維持するために ditto コマンドを使用する。
		system('/usr/bin/ditto', '--rsrc', $src, $dst);
		if ($? == -1) {
			Warn "%s: /usr/bin/ditto --rsrc '%s' '%s'",
				"$!", EscapedPath($src), EscapedPath($dst)
			;
			return 0;
		}
		elsif ($? & 127) {
			if ($? & 128) {
				Warn "Died with coredump: %d: /usr/bin/ditto --rsrc '%s' '%s'",
					($? & 127), EscapedPath($src), EscapedPath($dst)
				;
			}
			else {
				Warn "Died: %d: /usr/bin/ditto --rsrc '%s' '%s'",
					($? & 127), EscapedPath($src), EscapedPath($dst)
				;
			}
			return 0;
		}
		elsif ($?) {
			Warn "child exited: %d: /usr/bin/ditto --rsrc '%s' '%s'",
				$? >> 8, EscapedPath($src), EscapedPath($dst)
			;
			return 0;
		}
	}
	# その他の場合
	else {
		my	($sfh, $dfh);
		my	$buffer;
		my	$redLen;

		unless (open $sfh, '<', $src) {
			Warn "%s: %s", "$!", EscapedPath($src);
			return 0;
		}
		unless (open $dfh, '>', $dst) {
			close $sfh;
			Warn "%s: %s", "$!", EscapedPath($src);
			return 0;
		}

		while ($redLen = sysread $sfh, $buffer, $Conf{bufsiz}) {
			my	$wroteLen = syswrite $dfh, $buffer, $redLen;

			unless ($wroteLen == $redLen) {
				Warn "%s: %s", "$!", EscapedPath($src);
				close $sfh;
				close $dfh;
				return 0;
			}
		}
		unless (defined($redLen)) {
			Warn "%s: %s", "$!", EscapedPath($src);
			close $sfh;
			close $dfh;
			return 0;
		}
		close $sfh;
		unless (close $dfh) {
			# マウントしている場合など close に失敗することがある
			Warn "Can't close: %s", EscapedPath($dst);
			return 0;
		}
	}

	return 1;
}

#--- ディスクイメージのマウント/アンマウント ----
sub IsAbleDiskImage;
sub IsDiskImageFile;
sub MountDiskImage;
sub UnmountDiskImage;

sub _IsAbleDiskImage4darwin {
	1;
}

sub _IsAbleDiskImage4linux {
	my	$osver = eval {
		local	$^W = 0;

		`/bin/uname -r`
	};

	defined($osver) and $osver ge '2.4';
}

sub _IsAbleDiskImage4freebsd {
	-f '/sbin/mdconfig' and -x '/sbin/mdconfig'
}

*IsAbleDiskImage = eval "*_IsAbleDiskImage4$^O";

sub _IsDiskImageFile4darwin {
	my	($path) = @_;
	my	$retVal = 0;

	if (-f $path and $path =~ m/.+\.(dmg|iso|img|cdr|vhd)$/i) {
		$retVal = 1;
	}

	return $retVal;
}

sub _IsDiskImageFile {
	my	($path) = @_;

	if (-f $path and $path =~ m/.+\.iso$/i) {
		return 1;
	}
	else {
		return 0;
	}
}

sub _IsDiskImageFile4linux;
sub _IsDiskImageFile4freebsd;

*_IsDiskImageFile4linux = *_IsDiskImageFile;
*_IsDiskImageFile4freebsd = *_IsDiskImageFile;

*IsDiskImageFile = eval "*_IsDiskImageFile4$^O";

sub HdiutilError {
	my	($ret, $diskImage) = @_;

	$ret = $ret >> 8;
	if ($! == -1) {
		Error "%s: Cannot mount.", $diskImage;
	}
	else {
		if ($ret == 1) {	# EPERM
			Warn "%s: The refer operation cannot be performed.",
				$diskImage
			;
		}
		elsif ($ret == 6) {	# ENXIO
			Error "%s: Device not configured.", $diskImage;
		}
		elsif ($ret == 13) {	# EACCES
			Warn "%s: A particular access mode prevented the operation.",
				$diskImage
			;
		}
		elsif (
			$ret == 16	# EBUSY
		) {
			$! = 127 & $ret;
			Warn "%s: %s.", $diskImage, "$!";
		}
		elsif (
			$ret == 22	# EINVAL
				or
			$ret == 27	# EFBIG
				or
			$ret == 80	# EAUTH
		) {
			$! = 127 & $ret;
			Error "%s: %s.", $diskImage, "$!";
		}
		else {
			Error "%s: mount process exits with %d.", $diskImage, $ret;
		}
	}
}

sub _MountDiskImage4darwin {
	my	($diskImage, $mountPoint) = @_;
	my	$ret;

	Debug "mount %s to %s ...",  $diskImage, $mountPoint;
	if ($diskImage =~ m/\.vhd$/) {
		$ret = system(
			qw(/usr/bin/hdiutil attach
				-readonly -imagekey diskimage-class=CRawDiskImage
				-quiet -nobrowse -mountpoint
			),
			$mountPoint, $diskImage
		);
	}
	else {
		$ret = system(
			qw(
				/usr/bin/hdiutil attach
				-readonly -quiet -nobrowse -mountpoint
			),
			$mountPoint, $diskImage
		);
	}

	if ($ret) {
		HdiutilError($ret, ReplaceFName($diskImage));
		system(qw(/usr/bin/hdiutil detach -quiet -force), $mountPoint);
		return ();
	}
	else {
		Debug "done.";
		UpdateFileSystemInfo();
		return ($mountPoint);
	}
}

sub _UnmountDiskImage4darwin {
	my	($mountPoint) = @_;
	my	$ret;

	Debug "unmount %s ...",  $mountPoint;
	$ret = system(qw(/usr/bin/hdiutil detach -quiet -force), $mountPoint);
	if ($ret) {
		HdiutilError($ret, $mountPoint);
	}
	else {
		Debug "done.";
		UpdateFileSystemInfo();
	}

	return $ret == 0;
}

sub _MountDiskImage4linux {
	my	($diskImage, $mountPoint) = @_;
	my	$ret;
	my	@retVal;
	
	Debug "mount %s to %s ...",  $diskImage, $mountPoint;
	$ret = system(qw(mount -t iso9660 -o loop), $diskImage, $mountPoint);
	if ($ret == 0) {
		Debug "done.";
		UpdateFileSystemInfo();
		return ($mountPoint);
	}
	else {
		Error "%s: %s", ReplaceFName($diskImage), "$!";
		return ();
	}
}

sub _UnmountDiskImage4linux {
	my	($mountPoint) = @_;
	my	$ret;

	Debug "unmount %s ...",  $mountPoint;
	$ret = system('umount', $mountPoint);
	if ($ret == 0) {
		Debug "done.";
		UpdateFileSystemInfo();
		return 1;
	}
	else {
		Error "%s: %s", $mountPoint, "$!";
		return 0;
	}
}

sub _MountDiskImage4freebsd {
	my	($diskImage, $mountPoint) = @_;
	my	$devNum = 0;
	my	$ret;
	my	@retVal = ();

	Debug "mount %s to %s ...",  $diskImage, $mountPoint;
	while (-e "/dev/md$devNum") {
		$devNum++;
	}

	$ret = system(
		qw(/sbin/mdconfig -a -t vnode -f), $diskImage, '-u', $devNum
	);
	if ($ret == 0) {
		$ret = system(
			qw(/sbin/mount -t cd9660 -o ro), "/dev/md$devNum", $mountPoint
		);
		if ($ret == 0) {
			Debug "done.";
			UpdateFileSystemInfo();
			@retVal = ("/dev/md$devNum");
		}
		else {
			Error "%s: %s", ReplaceFName($diskImage), "$!";
			system(qw(/sbin/mdconfig -d -u), $devNum);
		}
	}
	else {
		Error "%s: %s", ReplaceFName($diskImage), "$!";
	}

	return @retVal;
}

sub _UnmountDiskImage4freebsd {
	my	($devFile) = @_;
	my	$ret;

	Debug "unmount %s ...",  $devFile;
	if (defined($devFile) and $devFile =~ m'^/dev/md(\d+)$') {
		my	$devNum = $1;

		$ret = system('/sbin/umount', $devFile);
		if ($ret == 0) {
			$ret = system(qw(/sbin/mdconfig -d -u), $devNum);
			if ($ret == 0) {
				Debug "done.";
				UpdateFileSystemInfo();
			}
			else {
				Error "%s: %s", $devFile, "$!";
			}
		}
		else {
			Error "%s: %s", $devFile, "$!";
		}
	}
	else {
		$ret = -1;
	}

	return $ret == 0;
}

*MountDiskImage = eval "*_MountDiskImage4$^O";
*UnmountDiskImage = eval "*_UnmountDiskImage4$^O";


#-------------------------------------------------------------------------------
# グローバル変数初期値
#-------------------------------------------------------------------------------
unless (exists $ENV{HOME} and defined($ENV{HOME})) {
	my	@pwent = getpwuid( $< );

	$ENV{HOME} = $pwent[7];
}
# 設定
%Conf = (
	#--- オリジナルオプション ----
	# スキャンデータのパス
	data	=> File::Spec->catfile($ENV{HOME}, '.'.$ProgramName),
	# スキャンデータを排他ロックするかどうか
	lock	=> 0,
	# ファイル監視期間
	period	=> 60*60*24*7*4,	# 4 週間
	# clamd ソケット
	'socket'	=> undef,	# Initialize でデフォルトを付与
	# clamd ホスト
	host	=> undef,	# Initialize でデフォルトを付与
	# clamd ポート
	port	=> undef,	# Initialize でデフォルトを付与
	# ストリーム送信最大長
	'stream-max-length'	=> undef,	# Initialize でデフォルトを付与
	# 非サポートオプションに警告するかどうか
	'warn-unsupported'	=> 1,
	# 非サポートオプションをエラーするかどうか
	'error-unsupported'	=> 0,
	# clamd 接続タイムアウト(秒)
	timeout	=> 10,
	# clamd スキャンタイムアウト(秒/MB)
	'scan-timeout'	=> 40,
	# clamd タイムアウト時のコマンド
	'timeout-command'	=> undef,
	# ストリーム送信バッファのサイズ(Byte)
	bufsiz	=> 4096,
	# CONTSCAN ではなく SCAN を使用するかどうか
	#	リモート clamd には影響しない
	contscan	=> 1,
	# スキャンデータルートのパーミッション
	'perm-root'	=> 0700,
	# スキャンデータ内のディレクトリのパーミッション
	'perm-dir'	=> 0700,
	# スキャンデータ内のファイルのパーミッション
	'perm-file'	=> 0600,
	# スキャンデータロックファイルのパーミッション
	'perm-lock'	=> 0600,
	# スキャンデータの保守モードかどうか
	'maintain-scan-data'	=> 0,
	# タイムアウト連続発生許容回数
	'limit-timeout'	=> 3,
	# リソースフォークをスキャンするかどうか
	rsrc	=> 0,
	# ディスクイメージ内をスキャンするかどうか
	dmg	=> undef,

	#--- clamdscan オプション ----
	# 冗長モード
	verbose	=> 0,
	# エラーのみ出力するかどうか
	quiet	=> 0,
	# エラー出力ではなく標準出力へ出力するかどうか
	stdout	=> 0,
	# clamd.conf のパス
	'config-file'	=> undef,
	# スキャンレポートを出力するファイル
	log	=> undef,
	# CONTSCAN ではなく MULTISCAN を使用するかどうか
	multiscan	=> 0,
	# 感染ファイルを削除するかどうか
	remove	=> 0,
	# 感染ファイルを隔離するディレクトリ
	move	=> undef,
	# サマリ出力をするかどうか
	summary	=> 1,

	#--- clamscan オプション ----
	# スキャン対象外ファイル
	exclude	=> [],
	# スキャン対象外ディレクトリ
	'exclude-dir'	=> [],
	# スキャン対象ファイル
	include	=> [],
	# スキャン対象ディレクトリ
	'include-dir'	=> [],
	# 再帰的にスキャンするかどうか
	recursive	=> 0,
	# 感染ファイルのみ出力するかどうか
	infected	=> 0,
	# 感染ファイルをコピーするディレクトリ
	copy	=> undef,
	# 感染発見時にベルを鳴らすかどうか
	'bell'	=> 0,
	# ディレクトリ最大深度
	'max-dir-recursion'	=> undef,	# Initialize でデフォルトを付与
);

# オプション
%Options = (
	# 情報表示のオプション
	infomation	=> {
		# ヘルプの表示
		'help|h'	=> sub { pod2usage(-verbose => 2) },
		# バージョンの表示
		'version|V'	=> sub {
			print <<EOF;
$Version
Copyright (C) 2007-2009 OKAMURA Yuji.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
			exit $ExitStatus{NoError};
		},
	},

	# オリジナルオプション
	original	=> {
		# スキャンデータのパス
		'data=s'	=> \$Conf{data},
		# スキャンデータを排他ロックするかどうか
		'lock'	=> \$Conf{lock},
		# チェック期間
		'period=i'	=> \$Conf{period},
		# clamd ソケット
		'socket=s'	=> \$Conf{'socket'},
		# clamd のホスト
		'host=s'	=> \$Conf{host},
		# clamd のポート
		'port=s'	=> \$Conf{port},
		# clamd のストリーム最大長
		'stream-max-length=s'	=> sub {
			my	($name, $value) = @_;

			if ($value =~ m/^(\d+)M$/i) {
				$Conf{'stream-max-length'} = 1024 * 1024 * $1;
			}
			elsif ($value =~ m/^(\d+)K$/i) {
				$Conf{'stream-max-length'} = 1024 * $1;
			}
			elsif ($value =~ m/^(\d+)$/) {
				$Conf{'stream-max-length'} = $1;
			}
			else {
				Error 'Unknown value format: --%s %s', , $name, $value;
				exit $ExitStatus{BadParams};
			}
		},
		# 非サポートオプションに警告するかどうか
		'warn-unsupported!'	=> \$Conf{'warn-unsupported'},
		# 非サポートオプションにエラーするかどうか
		'error-unsupported'	=> \$Conf{'error-unsupported'},
		# clamd 接続タイムアウト(秒)
		'timeout=i'	=> \$Conf{timeout},
		# clamd スキャンタイムアウト(秒/MB)
		'scan-timeout=i'	=> \$Conf{'scan-timeout'},
		# clamd タイムアウト時のコマンド
		'timeout-command=s'	=> \$Conf{'timeout-command'},
		# ストリーム送信バッファのサイズ
		'bufsiz'	=> sub {
			my	($name, $value) = @_;

			if ($value =~ m/^(\d+)$/) {
				$Conf{bufsiz} = $1;
			}
			elsif ($value =~ m/^(\d+)K$/i) {
				$Conf{bufsiz} = 1024 * $1;
			}
			elsif ($value =~ m/^(\d+)M$/i) {
				$Conf{bufsiz} = 1024 * 1024 * $1;
			}
			else {
				Error 'Value "%s" invalid for option %s', $value, $name;
				exit $ExitStatus{BadParams};
			}
		},
		# CONTSCAN ではなく SCAN を使用するかどうか
		'contscan!'	=> \$Conf{contscan},
		# スキャンデータルートパスのパーミッション
		'perm-root=s'	=> sub { $Conf{'perm-root'} = oct($_[1]); },
		# スキャンデータ内のディレクトリのパーミッション
		'perm-dir=s'	=> sub { $Conf{'perm-dir'} = oct($_[1]); },
		# スキャンデータ内のファイルのパーミッション
		'perm-file=s'	=> sub { $Conf{'perm-file'} = oct($_[1]); },
		# スキャンデータ内のロックァイルのパーミッション
		'perm-lock=s'	=> sub { $Conf{'perm-lock'} = oct($_[1]); },
		# スキャンデータの保守モードかどうか
		'maintain-scan-data'	=> \$Conf{'maintain-scan-data'},
		# タイムアウト連続発生許容回数
		'limit-timeout=i'	=> \$Conf{'limit-timeout'},
		# リソースフォークスキャンをするかどうか
		'scan-rsrc'	=> sub {
			my	($name, $value) = @_;

			if ($^O eq 'darwin' or defined $Sys::Filesystem::VERSION) {
				$Conf{rsrc} = 1;
			}
			else {
				Error 'Option "%s" requires Sys::Filesystem', $name;
				exit $ExitStatus{UnsupportedOpts};
			}
		},
		# ディスクイメージ内をスキャンするかどうか
		'scan-dmg'	=> sub {
			my	($name, $value) = @_;
			my	$isAbleDiskImage = 0;

			eval { $isAbleDiskImage = IsAbleDiskImage(); };
			if ($isAbleDiskImage) {
				$Conf{dmg} = 1;
			}
			else {
				Error "Sorry, %s is not supported on this system.", $name;
				exit $ExitStatus{UnsupportedOpts};
			}
		},
	},

	# clamdscan オプション
	clamdscan	=> {
		# 次は重複
		#	help|h, version|V

		# サポート対象
		supported	=> {
			# 冗長モード
			'verbose|v'	=> \$Conf{verbose},
			# エラーのみ出力するかどうか
			'quiet'			=> \$Conf{quiet},
			# エラー出力ではなく標準出力に出力するかどうか
			'stdout'	=> \$Conf{stdout},
			# clamd.conf のパス
			'config-file=s'	=> \$Conf{'config-file'},
			# スキャンレポートを出力するファイル
			'log|l=s'	=> \$Conf{log},
			# 感染ファイルを削除するかどうか
			'remove'	=> \$Conf{remove},
			# 感染ファイルを移動するディレクトリ
			'move=s'	=> sub {
				my	($name, $value) = @_;

				unless (SetMoveDir($name, $value)) {
					exit $ExitStatus{BadParams};
				}
			},
			# サマリ出力をしない
			'summary!'	=> \$Conf{'summary'},
			# CONTSCAN ではなく MULTISCAN をするかどうか
			'multiscan'	=> \$Conf{'multiscan'},
		},

		# サポート対象外
		unsupported	=> {
		},
	},

	# clamscan オプション
	clamscan	=> {
		# 次は重複
		#	help|h, version|V, log|l=s, summary!, remove, move=s

		# サポート対象
		supported	=> {
			# ディレクトリを再帰的にスキャンするかどうか
			'recursive|r'	=> \$Conf{recursive},
			# スキャン対象外ファイル
			'exclude=s'	=> sub {
				my	($name, $value) = @_;

				push @{$Conf{exclude}}, qr|$value|;
			},
			# スキャン対象外ディレクトリ
			'exclude-dir=s'	=> sub {
				my	($name, $value) = @_;

				push @{$Conf{'exclude-dir'}}, qr|$value|;
			},
			# スキャン対象ファイル
			'include=s'	=> sub {
				my	($name, $value) = @_;

				push @{$Conf{include}}, qr|$value|;
			},
			# スキャン対象ディレクトリ
			'include-dir=s'	=> sub {
				my	($name, $value) = @_;

				push @{$Conf{'include-dir'}}, qr|$value|;
			},
			# 感染ファイルのみを出力するかどうか
			'infected'	=> \$Conf{infected},
			# 感染ファイルをコピーするディレクトリ
			'copy=s'	=> sub {
				my	($name, $value) = @_;

				unless (SetMoveDir($name, $value)) {
					exit $ExitStatus{BadParams};
				}
			},
			# 感染発見時にベルを鳴らすかどうか
			'bell'	=> \$Conf{bell},
			# ディレクトリの最大階層深度
			'max-dir-recursion'	=> \$Conf{'max-dir-recursion'},
		},

		# サポート対象外
		unsupported	=> {
			# libclamav によるデバッグメッセージを表示するかどうか
			'debug'	=> \&WarnUnsupportedOption,
			# ウィルスデータベースのパス
			'database|d=s'	=> \&WarnUnsupportedOption,
			# 一時ディレクトリ
			'tempdir=s'	=> \&WarnUnsupportedOption,
			# 一時ファイルを残すかどうか
			'leave-temps'	=> \&WarnUnsupportedOption,
			# メールファイルのスキャンをするかどうか
			'mail!'	=> \&WarnUnsupportedOption,
			# シグネチャーベースのフィッシング判定をするかどうか
			'phishing-sigs'	=> \&WarnUnsupportedOption,
			# URL ベースのフィッシング判定をするかどうか
			'phishing-urls'	=> \&WarnUnsupportedOption,
			# 全てのドメインでフィッシング判定を有効にするかどうか
			'phishing-restrictedscan!'	=> \&WarnUnsupportedOption,
			# URL で SSL ミスマッチをいつもブロックするかどうか
			'phishing-ssl'	=> \&WarnUnsupportedOption,
			# 隠蔽 URL をブロックするかどうか
			'phishing-cloak'	=> \&WarnUnsupportedOption,
			# 特別なアルゴリズムをしようするかどうか
			algorithmic	=> \&WarnUnsupportedOption,
			# PE ファイル内をスキャンするかどうか
			'pe!'	=> \&WarnUnsupportedOption,
			# ELF ファイル内をスキャンするかどうか
			'elf!'	=> \&WarnUnsupportedOption,
			# OLE2 ファイル内をスキャンするかどうか
			'ole2!'	=> \&WarnUnsupportedOption,
			# PDF ファイル内をスキャンするかどうか
			'pdf!'	=> \&WarnUnsupportedOption,
			# HTML ファイル内をスキャンするかどうか
			'html!'	=> \&WarnUnsupportedOption,
			# アーカイブファイル内をスキャンするかどうか
			'archive!'	=> \&WarnUnsupportedOption,
			# 壊れた実行ファイルをウィルスとするかどうか
			'detect-broken'	=> \&WarnUnsupportedOption,
			# 暗号化されたアーカイブファイルをウィルスとするかどうか
			'block-encrypted'	=> \&WarnUnsupportedOption,
			# max-files, max-space, max-recursion に到達した
			# アーカイブファイルをウィルスとするかどうか
			'block-max'	=> \&WarnUnsupportedOption,
			# メール内の URL のリソースを検査するかどうか
			'mail-follow-urls'	=> \&WarnUnsupportedOption,
			# アーカイブ内の最大ファイル数
			'max-files=i'	=> \&WarnUnsupportedOption,
			# アーカイブ内の最大容量
			'max-space=i'	=> \&WarnUnsupportedOption,
			# アーカイブ内の最大階層深度
			'max-recursion=i'	=> \&WarnUnsupportedOption,
			# アーカイブの最大圧縮率
			'max-ratio=i'	=> \&WarnUnsupportedOption,
			# メールの最大階層深度
			'max-mail-recursion'	=> \&WarnUnsupportedOption,
			# unzip コマンドのパス
			'unzip=s'	=> \&WarnUnsupportedOption,
			# unrar コマンドのパス
			'unrar=s'	=> \&WarnUnsupportedOption,
			# arj コマンドのパス
			'arj=s'	=> \&WarnUnsupportedOption,
			# unzoo コマンドのパス
			'unzoo=s'	=> \&WarnUnsupportedOption,
			# lha コマンドのパス
			'lha=s'	=> \&WarnUnsupportedOption,
			# jar コマンドのパス
			'jar=s'	=> \&WarnUnsupportedOption,
			# debian バイナリパッケージコマンドのパス
			'deb=s'	=> \&WarnUnsupportedOption,
			# tar コマンドのパス
			'tar=s'	=> \&WarnUnsupportedOption,
			# tgz コマンドのパス
			'tgz=s'	=> \&WarnUnsupportedOption,
			# PUA 検出
			'detect-pua'	=> \&WarnUnsupportedOption,
			# PUA 検出除外カテゴリ
			'exclude-pua=s'	=> \&WarnUnsupportedOption,
			# PUA 検出対象カテゴリ
			'include-pua=s'	=> \&WarnUnsupportedOption,
		},
	},
);

# サマリの記録
%Summary = (
	# 時刻
	'time'	=> {
		# 開始
		start	=> undef,
		# 終了
		end		=> undef,
		# スキャン
		scan	=> {
			# 開始
			start	=> undef,
			# 終了
			end		=> undef,
		},
	},

	# カウント
	count	=> {
		# ファイル
		files	=> {
			# スキャン済み(クリーン+感染ではない)
			scaned		=> 0,
			# チェック済み
			checked		=> 0,
			# 感染
			infected	=> 0,
		},
		# ディレクトリ
		directories	=> 0,
		# 容量
		data	=> {
			scaned	=> 0,
			checked	=> 0,
		},
		# エラー発生回数
		error	=> 0,
	},
);

# 大域的記録
%ActionSwitch = (
	# 出力ストリーム
	outputStream	=> [],

	# スキャンデータのパスの長さ
	ScanDataBasePathLen	=> 0,

	# スキャンデータハンドル
	ScanDataHandle	=> undef,

	# 最古のスキャン対象の更新日時
	oldestTargetTime	=> 0,

	# スキャンタイプ
	scanType	=> undef,

	# スキャン関数
	ScanFunc	=> undef,

	# clamd スキャンコマンド
	ClamdScanCommand	=> undef,

	# ClamAV のバージョン
	ClamavVersion	=> {
		# 文字列表現
		string	=> undef,
		# ハッシュ表現
		hash	=> {ClamAV => undef, db => undef, dbBuildDateTime => undef},
	},

	# stat のキャッシュ
	statCache	=> undef,

	# 現在のディレクトリ深度
	dirDepth	=> undef,

	# clamd への IO::Socket オブジェクト
	socks	=> {},

	# timeout 連続発生回数
	timeoutFreq	=> 0,

	# ファイルシステム
	filessystem	=> [],

	# HFS フォーマットのファイルシステムのディレクトリ上かどうか
	onHfsDir	=> 0,

	# スキャン対象のファイルの前方置換スペック
	#	{
	#		prefix => 前方の文字列,
	#		prefixLen => 前方の文字列の長さ,
	#		replace => 置換文字列
	#	}
	targetPrefix	=> undef,
);

#-------------------------------------------------------------------------------
# 引数解釈
#-------------------------------------------------------------------------------
GetOptions(
	%{$Options{infomation}},
	%{$Options{original}},
	%{$Options{clamdscan}->{supported}},
	%{$Options{clamscan}->{supported}},
	%{$Options{clamdscan}->{unsupported}},
	%{$Options{clamscan}->{unsupported}},
) or pod2usage($ExitStatus{UknownOpts});


#-------------------------------------------------------------------------------
# メイン
#-------------------------------------------------------------------------------
if ($Conf{'maintain-scan-data'}) {
	unless (MaintainScanData()) {
		exit $ExitStatus{MaintainError};
	}
}
else {
	# 引数(スキャン対象)が指定されていなければカレントディレクトリを対象とする。
	unless (0 < scalar(@ARGV)) {
		push @ARGV, '.';
		$Conf{recursive} = 1;	# 再帰スキャンにしないと意味がない。
	}

	if (Initialize()) {
		if (scalar(@ARGV) == 1 and $ARGV[0] eq '-') {
			Error 'stdin scan is not supported.';
			exit $ExitStatus{BadArguments};
		}
		else {
			eval {
				foreach (@ARGV) {
					Scan($_);
				}
			};
			if ($@) {
				if ($@ =~ m/^Over frequence of continual clamd timeout\./) {
					Error 'Frequence of continual clamd timeout is over %d.',
						$Conf{'limit-timeout'}
					;
				}
			}
		}

		if (0 < $Summary{count}->{files}->{infected}) {
			exit $ExitStatus{ViruesFound};
		}
		if (0 < $Summary{count}->{error}) {
			exit $ExitStatus{SomeErrors};
		}
	}
	else {
		exit $ExitStatus{ScanError};
	}

	END {
		Terminate();
	}
}

#===============================================================================
__END__;

=head1 NAME

ClamdOmitScan.pl - High-speed anti-virus scanner for periodic scanning

=head1 SYNOPSIS

B<ClamdOmitScan.pl> [I<options>] [I<file>/I<directory> ...]

B<ClamdOmitScan.pl> --maintain-scan-data [I<options>]

B<ClamdOmitScan.pl> --help|-h

B<ClamdOmitScan.pl> --version|-V

=head1 DESCRIPTION

B<ClamdOmitScan.pl> is a high-speed anti-virus scanner for periodic scanning.

When I<file> or I<directory> are omited, B<ClamdOmitScan.pl> scans current
directory.

B<ClamdOmitScan.pl> scans files or directories by using clamd like clamdscan.
But it behaves like clamscan. It accepts all the options implemented in
clamdscan or clamscan. Though most of clamscan options will be ignored like
clamdscan, it uses options more than clamdscan.

In addition, B<ClamdOmitScan.pl> scans files or directories that has been
scanned once at high speed. Because it omits the scanning of files which are not
need to be scanned.

clamdscan can not connects to remote clamd. But B<ClamdOmitScan.pl> can connect
it. You can use anti-virus scan server on LAN. You need not install ClamAV in
each client that uses B<ClamdOmitScan.pl>.

NOTE: B<ClamdOmitScan.pl> does not support stdin scanning it though clamscan
supports it.

=head1 WHAT IS NECESSITY TO SCAN

B<ClamdOmitScan.pl> inspects the following conditions before scanning a file.

=over 4

=item C1. File Timestamp Condition

The file was modified or created after last scanned time.

=item C2. Scanner Version Condition

The version of current ClamAV and the version of it when the file was last
scanned are different.

=item C3. Inspection Period Condition

The timestamp of the file is not older enough than the timestamp of last ClamAV
which scanned it.

=back

The file must be scanned If and only if C1 or (C2 and C3).

NOTE: Default enough period of C3 is 4 weeks.
See L<--period option|/"item__2d_2dperiod_seconds__7c__2d_2dperiod_3dseconds">.

=head1 INET SOCKET

B<ClamdOmitScan.pl> uses INET socket in the following cases.

=over 2

* L<--socket|/"item__2d_2dsocket_path"> is not specified.

* AND C<LocalSocket> value is not specified in F<clamd.conf>.

* AND L<--port|/"item__2d_2dport_port__7c__2d_2dport_3dport"> is specified, or
C<TCPSocket> value is specified in F<clamd.conf>.

=back

=head1 STREAM CLAMD COMMAND

B<ClamdOmitScan.pl> uses C<STREAM> clamd command in the following cases.

=over 2

* B<ClamdOmitScan.pl> uses INET socket to connect to clamd.

* OR line-feed is included in the filename of the file to be scanned.

=back

=head1 OPTIONS

=head2 ORIGINAL OPTIONS

=over

=item --maintain-scan-data

Maintain scanning data. When this option is specified, B<ClamdOmitScan.pl>
does not scan.

=item --data I<directory> | --data=I<directory>

Path of scanning data directory. F<I<directory>> and F<I<directory>.lock> will
be created if they don't exist.

DEFAULT: F<~/.ClamdOmmitScan>

=item --lock

Use exclusive lock for locking the scanning data. If this isn't specified,
B<ClamdOmmitSca.pl> uses shared lock. See L<flock(2)>.

=item --period I<seconds> | --period=I<seconds>

File inspection period. See L</WHAT IS NECESSITY TO SCAN>.

DEFAULT: 2419200 (4 weeks)

=item --socket I<path>

clamd UNIX domain socket path. When this option is specified,
B<ClamdOmitScan.pl> uses UNIX domain socket to connect to clamd.

When this option is not specified, B<ClamdOmitScan.pl> uses C<LocalSocket>
value, if it is written in F<clamd.conf>.

DEFAULT: F</tmp/clamd.socket>

=item --port I<port> | --port=I<port>

clamd INET socket (port number). When B<ClamdOmitScan.pl> uses INET socket to
connect to clamd, it uses this I<port> as port number.

When this option is not specified, B<ClamdOmmitSca.pl> uses C<TCPSocket> value,
if it is written in F<clamd.conf>.

DEFAULT: (no)

=item --host I<address> | --host=I<address>

clamd server address.

When this option is not specified, B<ClamdOmmitSca.pl> uses C<TCPAddr> value,
if it is written in F<clamd.conf>.

DEFAULT: C<127.0.0.1>

=item --stream-max-length I<length>
| --stream-max-length=I<length>

See L<clamd.conf(5)/StreamMaxLength>.

When this option is not specfied, B<ClamdOmmitScan.pl> uses C<StreamMaxLength>
value, if it is written in F<clamd.conf>.

B<WARNING>: B<ClamdOmitScan.pl> might die when I<length> is larger than the
value clamd actually used. They are the following cases.

=over 2

* B<ClamdOmitScan.pl> uses STREAM clamd command.

* AND the size of the file to be scanned is larger than I<length>.

=back

See L</STREAM CLAMD COMMAND>.

DEFAULT: 10M

=item --no-warn-unsupported

In the default, B<ClamdOmmitSca.pl> warns about using unsuported options of
clamscan. If this option is specified, it doesn't warns.

=item --error-unsupported

In the default, B<ClamdOmmitSca.pl> warns about using unsuported options of
clamscan. If this option is specified, it treats as a error.

=item --timeout I<seconds> | --timeout=I<seconds>

clamd connection timeout.

DEFAULT: 10

=item --scan-timeout I<seconds> | --scan-timeout=I<seconds>

Scan timeout par 1MB.

DEFAULT: 40

=item --bufsiz I<length> | --bufsiz=I<length>

When B<ClamdOmmitSca.pl> uses C<STREAM> clamd command or copies a infected file,
it uses this value as read/write buffer size.

DEFAULT: 4096

=item --no-contscan

If this option is specified, B<ClamdOmmitSca.pl> uses C<SCAN> clamd commnand
instead of C<CONTSCAN> clamd command. See L<clamd(8)/SCAN> and
L<clamd(8)/CONTSCAN>.

=item --perm-root I<octet> | --perm-root=I<octet>

Scanning data root directory permittion.

DEFAULT: 0700

=item --perm-dir I<octet> | --perm-dir=I<octet>

Directories permittion in scanning data directory.

DEFAULT: 0700

=item --perm-file I<octet> | --perm-file=I<octet>

Files permittion in scanning data directory.

DEFAULT: 0600

=item --perm-lock I<octet> | --perm-lock=I<octet>

Scanning data lock file permittion.

DEFAULT: 0600

=item --limit-timeout I<num> | --limit-timeout=I<num>

Frequency limitation of consecutive timeout. When clamd timeout occurred
continualy I<num>+1 or more times, B<ClamdOmitScan.pl> stops if I<num> is 0 or
more.

Increase the value of
L<--timeout|/"item__2d_2dtimeout_seconds__7c__2d_2dtimeout_3dseconds"> or
L<--scan-timeout|/"item__2d_2dscan_2dtimeout_seconds__7c__2d_2dscan_2dtime">
when B<ClamdOmitScan.pl> stops for this limitation though clamd operates
normally.

DEFAULT: 3

=item --scan-rsrc

Scan a resource fork of each file on HFS filesystem.

When your system is not Darwin(Mac OS X),
this option requires perl module L<Sys::Filesystem|Sys::Filesystem>.

DEFAULT: (no)

=item --scan-dmg

Treat disk image files as directory. B<ClamdOmitScan> mounts a disk image file
to temporary directory, and scans files under it. This option is supported on
Darwin(Mac OS X), Linux, FreeBSD.

If your system is Linux, kernel version must be equal or greater than 2.4.
If your system is FreeBSD, F</sbin/mdconfig> must be installed.
Both of Linux and FreeBSD require the perl module L<Sys::Filesystem|Sys::Filesystem>.

Supported type of disk image file are the following:

    Darwin(Mac OS X): .iso, .dmg, .img, .cdr, .vhd(Virual PC disk image)
    the other OSs: .iso

NOTE: Disk image files for which agreement of license is necessary or encrypted
disk image files are supported on only TTY environments.

DEFALUT: (no)

=back

=head2 CLAMDSCAN OPTIONS

All clamdscan options are supported. See L<clamdscan(1)> for detail.

=head2 CLAMSCAN OPTIONS

The following clamscan options are supported.

=over

=item --recursive | -r

=item --max-dir-recursion

=item --exclude I<pattern> | --exclude=I<pattern>

=item --exclude-dir I<pattern> | --exclude-dir=I<pattern>

=item --include I<pattern> | --include=I<pattern>

=item --include-dir I<pattern> | --include-dir=I<pattern>

=item --infected

=item --copy I<directory> | --copy=I<directory>

=item --bell

=back

See L<clamscan(1)> for detail.

=head1 EXAMPLE

(0) Scan a single file:

=over

C<ClamdOmitScan.pl file>

=back

(1) Scan a current working directory:

=over

C<ClamdOmitScan.pl>

=back

(2) Scan all files (and subdirectories) in /home:

=over

C<ClamdOmitScan.pl -r /home>

=back

(3) Use custom scanning data directory:

=over

C<ClamdOmitScan.pl --data=/var/ClamdOmitScan/scanData>

=back

(4) Maintain custom scanning data directory:

=over

C<ClamdOmitScan.pl --data=/var/ClamdOmitScan/scanData --maintain-scan-data>

=back

=head1 RETURN CODES

0: No virus found.

1: Virus(es) found.

2: Some errors occured.

40: Unknown option passed.

41: Bad options value passed.

42: Bad argument passed.

43: Unsupported option passed.

50: Error occured for scanning.

51: Error occured for maintenance scan data.

=head1 SEE ALSO

L<clamd(8)>, L<clamdscan(1)>, L<clamscan(1)>, L<flock(2)>, L<clamd.conf(5)>,
L<Sys::Filesystem|Sys::Filesystem>

=head1 AUTHOR

OKAMURA Yuji E<lt>https://sourceforge.jp/users/okamura/E<gt>

=cut
