#!/bin/bash -e

shopt -s nullglob

export PATH=$PATH:/opt/7zip

parseHtaccess() {
	header="$(grep " $1" .htaccess | grep AddDescription | cut -d'"' -f2)"
	icon="$(grep " $1" .htaccess | grep AddIcon | cut -d' ' -f2)"
	title="$(echo "${header}" | sed 's#<br/>.*##' | sed 's|<[^>]*>||g')"
}

generateHeader() {
	suffix=""
	rootDesc=""
	if fileoutput="$(file -bs "$1")"; then
		rootDesc="${fileoutput}"
	fi
}

fileDetails() {
	for (( i = 1; i <= $indentlevel; i++ )); do
		echo -n " "
	done
	echo -n " "
	#stat --printf='%A %u %g %10s %-.19y  %N' -- "${file}" | sed 's#&#\&amp;#g; s#<#\&lt;#g;'
        echo "</samp><br/>"
}

processFiles() {
echo '<?php
$now = time();
$xw = new XMLWriter();

function storeCommonAttrs($file, $time, $stats) {
	global $xw;
	if (preg_match("/^./us", $file) == 0) {
		// Try to convert file name, but this is just a guess and will fail e.g. with s-br�cke 0.mp3 in /goodies/scenario/scenario.zip of AgeOfEmpiresIIGoldEditionCD2.raw
		// Guessing will always be unreliable. Maybe stop on such files and present some options?
		echo("Converting non-UTF-8 filename " . $file . ".\n");
		$file = mb_convert_encoding($file, "UTF-8", "ISO-8859-1");
	}
	$file = preg_replace("/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]/u", "�", $file);
	$xw->writeElement("name", $file);
	$xw->writeElement("mtime", $time);
	$xw->writeElement("size", $stats["size"]);
	$xw->writeElement("mode", $stats["mode"]);
	$xw->writeElement("user", $stats["uid"]);
	$xw->writeElement("group", $stats["gid"]);
}

function processFiles($dir) {
	global $finfo, $now, $xw;
	if ($dh = opendir($dir)) {
		while (($file = readdir($dh)) !== false) {
			if ($file != "." && $file != "..") {
				$isMount = false;
				$dirfile = $dir . "/" . $file;
				$mode = filetype($dirfile);
				$stats = lstat($dirfile);
				$time = $stats["mtime"];
				// Some archives do not have timestamps stored, e.g. ZIP files often do not contain
				// timestamps for directories; the timestamp will then match the extraction date, so
				// filter these entries out. However there are also discs with timestamps in the
				// future, those should be preserved.
				if ($time >= $now && $time <= $now + 1800) {
					$time = "";
				}
				if($mode === "dir") {
					$xw->startElement("dir");
					storeCommonAttrs($file, $time, $stats);
					$xw->writeElement("mime", mime_content_type($dirfile));
					$xw->startElement("contents");
					processFiles($dirfile);
					$xw->endElement();
					$xw->endElement();
				} elseif($mode === "link") {
					$link = readlink($dirfile);
					# When extracting files with an absolute path, 7-Zip occasionally adds the current working directory to the root path - remove that.
					# Example: links.tgz in SuSELinuxNovember1995_x86_de_CD3.cue
					$link = preg_replace("#^/tmp/generateFileListing-[^/]+/extractedFilesOf_[^/]+/#", "/", $link);

					$xw->startElement("link");
					storeCommonAttrs($file, $time, $stats);
					$xw->writeElement("mime", "inode/symlink");
					$xw->writeElement("target", $link);
					$xw->endElement();
				} elseif($mode === "file") {
					$retval = 1;
					$magic = finfo_file($finfo, $dirfile);
					$mime = mime_content_type($dirfile);
					$temp_dir = sys_get_temp_dir() . "/extractedFilesOf_" . mt_rand() . "/" . $file;
					if (! mkdir($temp_dir, 0777, true)) {
						echo "Could not create temporary directory $temp_dir";
						$temp_dir = $dirfile;
					}
					// 7-Zip´s Linux format support is quite lacking - it does not support
					// timestamps for symlinks, special devices such as block or character
					// devices, and occasionally adds the current working directory to the
					// root path. Use bsdtar / cpio instead.
					// Example: links.tgz in SuSELinuxNovember1995_x86_de_CD3.cue
					//          DEVS.TGZ in SlackWare112BasispaketA_x86_de_Floppy3.img
					else if (in_array($mime, array("application/x-tar"))) {
						exec("/usr/bin/bsdtar xf " . escapeshellarg($dirfile) . " -C " . escapeshellarg($temp_dir), result_code: $retval);
						if ($retval !== 0) {
							echo "Failed in $dirfile\n";
						}
					} else if (in_array($mime, array("application/x-cpio"))) {
						exec("cpio --extract --quiet --preserve-modification-time --make-directories --directory=" . escapeshellarg($temp_dir) . " < " . escapeshellarg($dirfile), result_code: $retval);
						if ($retval !== 0) {
							echo "Failed in $dirfile\n";
						}
					} else if ($mime === "application/x-installshield" && strtolower(pathinfo($dirfile)["extension"]) != "hdr") {
						exec("unshield -d " . escapeshellarg($temp_dir) . " x " . escapeshellarg($dirfile), result_code: $retval);
						if ($retval !== 0) {
							// Try old compression method
							exec("unshield -O -d " . escapeshellarg($temp_dir) . " x " . escapeshellarg($dirfile), result_code: $retval);
							if ($retval !== 0) {
								echo "Failed in $dirfile\n";
							}
						}
					} else if ($mime === "application/octet-stream" && $magic === "InstallShield Z archive Data") {
						exec("unshieldv3 extract " . escapeshellarg($dirfile) . " " . escapeshellarg($temp_dir), result_code: $retval);
						if ($retval !== 0) {
							echo "Failed extracting $dirfile with unshieldv3\n";
						}
					} else if ($mime === "application/octet-stream" && (str_contains($magic, "filesystem data") || str_starts_with($magic, "Minix filesystem"))) {
						exec("mount -o loop,ro " . escapeshellarg($dirfile) . " " . escapeshellarg($temp_dir), result_code: $retval);
						if ($retval === 0) {
							$isMount = true;
						} else {
							echo "Failed in $dirfile\n";
						}
					} else if (! in_array($mime, array("text/plain", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.visio", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.oasis.opendocument.presentation", "application/vnd.oasis.opendocument.spreadsheet", "application/vnd.oasis.opendocument.text", "application/x-ole-storage", "application/vnd.ms-office")) &&
					    ! in_array($file, array("Thumbs.db"))) {
						exec("7zz x -stxAPM -stxBase64 -stxCOFF -stxFLV -stxELF -stxGPT -stxIHex -stxLP -stxMBR -stxMachO -stxMub -stxPE -stxSWF -stxSWFc -stxUEFIc -stxUEFIf -stxHash -snoi -snld -o" . escapeshellarg($temp_dir) . " " . escapeshellarg($dirfile) . " 2>/dev/null", result_code: $retval);
						if ($retval != 0 && isset(pathinfo($dirfile)["extension"]) && strtolower(pathinfo($dirfile)["extension"]) == "exe") {
							exec("innoextract --output-dir " . escapeshellarg($temp_dir) . " " . escapeshellarg($dirfile) . " 2>/dev/null", result_code: $retval);
						}
					}
					$iterator = new FilesystemIterator($temp_dir);
					// if ($retval != 0) {
					if (! $iterator->valid()) {
						$xw->startElement("file");
						storeCommonAttrs($file, $time, $stats);
						$xw->writeElement("magic", $magic);
						$xw->writeElement("mime", $mime);
						$xw->endElement();
					} else {
						$xw->startElement("archive");
						storeCommonAttrs($file, $time, $stats);
						$xw->writeElement("magic", $magic);
						$xw->writeElement("mime", $mime);
						$xw->startElement("contents");
						processFiles($temp_dir);
						$xw->endElement();
						$xw->endElement();
					}
					if ($isMount == true) {
						exec("umount " . escapeshellarg($temp_dir), result_code: $retval);
					}
					exec(sprintf("rm -rf %s 2>/dev/null", escapeshellarg($temp_dir)));
				} else {
						$xw->startElement("special");
						storeCommonAttrs($file, $time, $stats);
						$xw->writeElement("mime", $mode);
						$xw->endElement();
				}
			}
		}
		closedir($dh);
	}
}

$finfo = finfo_open(FILEINFO_NONE);

// Make sure symlinks are deleted first
if (file_exists($argv[3])) {
	unlink($argv[3]);
}

$xw->openUri($argv[4]);
$xw->setIndent(true);
$xw->startDocument();
$xw->writePi("xml-stylesheet", "type=\"text/xsl\" href=\"imageviewer.xsl\"");
$xw->startElement("image");
$xw->startElement("title");
$xw->text($argv[1]);
$xw->endElement();
$xw->startElement("desc");
$xw->writeRaw($argv[3]);
$xw->endElement();
$xw->startElement("magic");
$xw->text($argv[2]);
$xw->endElement();
$xw->writeElement("iconref", "");
$xw->startElement("contents");
processFiles(".");
$xw->endElement();
$xw->endDocument();

finfo_close($finfo);
?>' | php -- "${title}" "${rootDesc}" "${header}" "${outfile}"
}

generateFileListing() {
	outfile="$(pwd)/${outputName}${suffix}.xml"
	rm -f "${outfile}"

	pushd "${mnt}" >/dev/null
	if [ -z "${linksTo}" ]; then
		processFiles
		popd >/dev/null
	else
		popd >/dev/null
		cp "${linksTo}${suffix}.xml" "${outfile}"
	fi
	sed -i 's#^ <iconref>.*</iconref># <iconref>'${icon}'</iconref>#' "${outfile}"
	umount "${mnt}"
}

generateFileListingLoop() {
	generateHeader "${dev}"
	for blockdev in "${dev}"*; do
		if mount -o ro "${blockdev}" "${mnt}" 2>/dev/null; then
			partition="${blockdev#$dev}"
			if [ -n "${partition}" ]; then
				suffix="-${partition}"
				rootDesc="$(file -bs "${blockdev}")"
			fi
			echo "Device: ${blockdev}"
			generateFileListing
		fi
	done
}

generateFileListingOptical() {
	generateHeader "${dev}"
	rm -f "${outputName}"-*.xml
	# Try various possible file systems and check for differences (Hybrid Disc)
	if mount -t udf -o ro "${dev}" "${mnt}" 2>/dev/null; then
		echo "Found UDF file system"
		suffix="-udf"
		generateFileListing
	fi
	if isoinfo -d -i "${dev}" 2>/dev/null | grep -q -e '^Joliet .* found.$'; then
		mount -t iso9660 -o ro,norock "${dev}" "${mnt}"
		echo "Found ISO 9660 file system with Joliet extensions"
		suffix="-iso9660-joliet"
		generateFileListing
	fi
	if isoinfo -d -i "${dev}" 2>/dev/null | grep -q -e 'Rock Ridge signatures .* found'; then
		mount -t iso9660 -o ro,nojoliet "${dev}" "${mnt}"
		echo "Found ISO 9660 file system with Rock Ridge signatures"
		suffix="-iso9660-rock"
		generateFileListing
	fi
	if [ ! -e "${outputName}-iso9660-joliet.xml" -a ! -e "${outputName}-iso9660-rock.xml" ] && mount -t iso9660 -o ro "${dev}" "${mnt}" 2>/dev/null; then
		echo "Found ISO 9660 file system"
		suffix="-iso9660"
		generateFileListing
	fi
	if mount -t hfsplus -o ro "${dev}" "${mnt}" 2>/dev/null; then
		echo "Found HFS+ file system"
		suffix="-hfsplus"
		generateFileListing
	fi
	mount -t tmpfs tmpfs "${mnt}"
	if /opt/hfsexplorer/bin/unhfs -o "${mnt}" -resforks APPLEDOUBLE "${dev}" 2>/dev/null; then
		echo "Found HFS file system (using HFSExplorer)"
		suffix="-hfs"
		hfsexplorersuccess=1
		generateFileListing
	else
		umount "${mnt}"
	fi
	# HFSExplorer cannot mount all HFS file systems mount can (and vice versa), so try again
	if [ ! -e "${outputName}-hfs.xml" ] && mount -t hfs -o ro "${dev}" "${mnt}" 2>/dev/null; then
		echo "Found HFS file system (using mount)"
		suffix="-hfs"
		generateFileListing
	fi
	linksTo=""
}

initLoop() {
	dev="$(losetup -f)"
	losetup -P "${dev}" "${image}"
}

processDisc() {
	echo "Processing ${image[@]}..."
	imagedata="${image}"
	imagedata="${imagedata/.cue/.bin}"
	imagedata="${imagedata/.toc/.bin}"
	parseHtaccess "${imagedata}"
	initOptical
	generateFileListingOptical
	teardownOptical
}

initOptical() {
	if [ -L "${imagedata}" ]; then
		linksTo="$(readlink "${imagedata}")"
		linksTo="${linksTo%%.*}.${image##*.}"
	fi
	dev=""
	# cdemu's dbus interface returns before the disc is actually unloaded
	# (resulting in an error when trying to mount the same device again),
	# so try again until is succeeds as a workaround
	while ! cdemu load ${devnum} "${image[@]}"; do
		sleep 0.5
	done
	while [ -z "${dev}" ]; do
		dev="$(cdemu device-mapping | tail -1 | awk '{print $2}')"
		sleep 0.5
	done

}

teardownLoop() {
	losetup -d "${dev}"
}

teardownOptical() {
	cdemu unload ${devnum}
	true
}

cleanup() {
	umount "${mnt}" 2>/dev/null || true
	umount -R "${tmpdir}"
	rm -rf "${tmpdir}"
	#cdemu remove-device
	exit
}

processImage() {
	outputName="${image}"
	# Magnetical media images
	if [[ $image == *\.img ]] || [[ $image == *\.st ]] || [[ $image == *\.adf ]]; then
		echo "Processing ${image}..."
		parseHtaccess "${image}"
		initLoop
		generateFileListingLoop
		teardownLoop
	# Optical disc images
	elif [[ $image == *\.cue ]] || [[ $image == *\.iso ]] || [[ $image == *\.raw ]] || [[ $image == *\.nrg ]]; then
		# Multisession discs
		if [[ $image == *_session1\.cue ]]; then
			outputName="${image%%_session1.cue}.toc"
			image=(${image%%_session1.cue}_session*.toc)
		elif [[ $image == *_session*\.cue ]]; then
			return
		fi

		processDisc
	fi
}


trap 'cleanup' 0

tmpdir=$(mktemp -d -t generateFileListing-XXXXXXXXXX)
mount -t tmpfs tmpfs "${tmpdir}"
export TMPDIR="${tmpdir}"
mnt="${tmpdir}/mountpoint"
mkdir "${mnt}"

cdemu add-device
devnum=$(cdemu device-mapping | tail -1 | cut -f 1 -d " ")

if [ -z "$1" ]; then
	for image in *.{img,st,adf,cue,iso,raw,nrg}; do
		processImage "${image}"
	done
else
	for image; do
		processImage "${image}"
	done
fi

echo "Done!"
