#!/bin/bash
#
# SATA SSD free-space TRIM utility, by Mark Lord <mlord@pobox.com>

VERSION=3.5 

# Copyright (C) 2009-2010 Mark Lord.  All rights reserved.
#
# Contains hfsplus and ntfs code contributed by Heiko Wegeler <heiko.wegeler@googlemail.com>.
# Package sleuthkit version >=3.1.1 is required for HFS+. Package ntfs-3g and ntfsprogs is required for NTFS.
#
# Requires gawk, a really-recent hdparm, and various other programs.
# This needs to be redone entirely in C, for 64-bit math, someday.
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License Version 2,
# as published by the Free Software Foundation.
# 
# This program is distributed in the hope that it would be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Note for OCZ Vertex-LE users:  the drive firmware will error when
# attempting to trim the final sector of the drive.  To avoid this,
# partition the drive such that the final sector is not used.

export LANG=C

## The usual terse usage information:
##
function usage_error(){
	echo >&2
	echo "Linux tune-up (TRIM) utility for SATA SSDs"
	echo "Usage:  $0 [--verbose] [--commit] <mount_point|block_device>" >&2
	echo "   Eg:  $0 /dev/sda1" >&2
	echo >&2
	exit 1
}

## Parameter parsing for the main script.
## Yeah, we could use getopt here instead, but what fun would that be?
##

echo
echo "${0##*/}: Linux SATA SSD TRIM utility, version $VERSION, by Mark Lord."

export verbose=0
commit=""
destroy_me=""
argc=$#
arg=""
while [ $argc -gt 0 ]; do
	if [ "$1" = "--commit" ]; then
		commit=yes
	elif [ "$1" = "--please-prematurely-wear-out-my-ssd" ]; then
		destroy_me=yes
	elif [ "$1" = "--verbose" ]; then
		verbose=$((verbose + 1))
	elif [ "$1" = "" ]; then
		usage_error
	else
		if [ "$arg" != "" ]; then
			echo "$1: too many arguments, aborting." >&2
			exit 1
		fi
		arg="$1"
	fi
	argc=$((argc - 1))
	shift
done
[ "$arg" = "" ] && usage_error

## Find a required program, or else give a nicer error message than we'd otherwise see:
##
function find_prog(){
	prog="$1"
	if [ ! -x "$prog" ]; then
		prog="${prog##*/}"
		p=`type -f -P "$prog" 2>/dev/null`
		if [ "$p" = "" ]; then
			[ "$2" != "quiet" ] && echo "$1: needed but not found, aborting." >&2
			exit 1
		fi
		prog="$p"
		[ $verbose -gt 0 ] && echo "  --> using $prog instead of $1" >&2
	fi
	echo "$prog"
}

## Ensure we have most of the necessary utilities available before trying to proceed:
##
hash -r  ## Refresh bash's cached PATH entries
HDPARM=`find_prog /sbin/hdparm`	|| exit 1
FIND=`find_prog /usr/bin/find`	|| exit 1
STAT=`find_prog /usr/bin/stat`	|| exit 1
GAWK=`find_prog /usr/bin/gawk`	|| exit 1
BLKID=`find_prog /sbin/blkid`	|| exit 1
GREP=`find_prog /bin/grep`	|| exit 1
ID=`find_prog /usr/bin/id`	|| exit 1
LS=`find_prog /bin/ls`		|| exit 1
DF=`find_prog /bin/df`		|| exit 1
RM=`find_prog /bin/rm`		|| exit 1
STAT=`find_prog /usr/bin/stat`	|| exit 1

[ $verbose -gt 1 ] && HDPARM="$HDPARM --verbose"

## I suppose this will confuse the three SELinux users out there:
##
if [ `$ID -u` -ne 0 ]; then
	echo "Only the super-user can use this (try \"sudo $0\" instead), aborting." >&2
	exit 1
fi

## We need a very modern hdparm, for its --fallocate and --trim-sector-ranges-stdin flags:
## Version 9.25 added automatic determination of safe max-size of TRIM commands.
##
HDPVER=`$HDPARM -V | $GAWK '{gsub("[^0-9.]","",$2); if ($2 > 0) print ($2 * 100); else print 0; exit(0)}'`
if [ $HDPVER -lt 925 ]; then
	echo "$HDPARM: version >= 9.25 is required, aborting." >&2
	exit 1
fi

## Convert relative path "$1" into an absolute pathname, resolving all symlinks:
##
function get_realpath(){
	iter=0
	p="$1"
	while [ -e "$p" -a $iter -lt 100 ]; do
		## Strip trailing slashes:
		while [ "$p" != "/" -a "$p" != "${p%%/}" ]; do
			p="${p%%/}"
		done
		## Split into directory:leaf portions:
		d="${p%/*}"
		t="${p##*/}"
		## If the split worked, then cd into the directory portion:
		if [ "$d" != "" -a "$d" != "$p" ]; then
			cd -P "$d" || exit
			p="$t"
		fi
		## If what we have left is a directory, then cd to it and print realpath:
		if [ -d "$p" ]; then
			cd -P "$p" || exit
			pwd -P
			exit
		## Otherwise if it is a symlink, read the link and loop again:
		elif [ -h "$p" ]; then
			p="`$LS -ld "$p" | awk '{sub("^[^>]*-[>] *",""); print}'`"
		## Otherwise, prefix $p with the cwd path and print it:
		elif [ -e "$p" ]; then
			[ "${p:0:1}" = "/" ] || p="`pwd -P`/$p"
			echo "$p"
			exit
		fi
		iter=$((iter + 1))
	done
}

function get_devpath(){
	dir="$1"
	kdev=`$STAT --format="%04D" "$dir" 2>/dev/null`
	[ "$kdev" = "" ] && exit 1
	major=$((0x${kdev:0:2}))
	minor=$((0x${kdev:2:2}))
	$FIND /dev -xdev -type b -exec $LS -ln {} \; | $GAWK -v major="$major," -v minor="$minor" \
		'($5 == major && $6 == minor){r=$NF}END{print r}'
}

## Convert "$arg" into an absolute pathname target, with no symlinks or embedded blanks:
target="`get_realpath "$arg"`"
if [ "$target" = "" ]; then
	[ "$arg" = "/dev/root" ] && target="`get_devpath /`"
	if [ "$target" = "" ]; then
		echo "$arg: unable to determine full pathname, aborting." >&2
		exit 1
	fi
fi
if [ "$target" != "${target##* }" ]; then
	echo "\"$target\": pathname has embedded blanks, aborting." >&2
	exit 1
fi

## Take a first cut at online/offline determination, based on the target:
##
if [ -d "$target" ]; then
	method=online
elif [ -b "$target" ]; then
	method=offline
else
	echo "$target: not a block device or mount point, aborting." >&2
	exit 1
fi

## Find the active mount-point (fsdir) associated with a device ($1: fsdev).
## This is complicated, and probably still buggy, because a single
## device can show up under *multiple* mount points in /proc/mounts.
##
function get_fsdir(){
	rw=""
	r=""
	while read -a m ; do
		pdev="${m[0]}"
		[ "$pdev" = "$1" ] || pdev="`get_realpath "$pdev"`"
		if [ "$pdev" = "$1" ]; then
			if [ "$rw" != "rw" ]; then
				rw="${m[3]:0:2}"
				r="${m[1]}"
			fi
		fi
		#echo "$pdev ${m[1]} ${m[2]} ${m[3]}"
	done
	echo -n "$r"
}

## Find the device (fsdev) associated with a mount point ($1: fsdir).
## Since mounts can be stacked on top of each other, we return the
## one from the last occurance in the list from /proc/mounts.
##
function get_fsdev(){   ## from fsdir
	get_realpath "`$GAWK -v p="$1" '{if ($2 == p) r=$1} END{print r}' < /proc/mounts`"
}

## Find the r/w or r/o status (fsmode) of a filesystem mount point  ($1: fsdir)
## We get it from the last occurance of the mount point in the list from /proc/mounts,
## and convert it to a longer human-readable string.
##
function get_fsmode(){  ## from fsdir
	mode="`$GAWK -v p="$1" '{if ($2 == p) r=substr($4,1,2)} END{print r}' < /proc/mounts`"
	if [ "$mode" = "ro" ]; then
		echo "read-only"
	elif [ "$mode" = "rw" ]; then
		echo "read-write"
	else
		echo "$fsdir: unable to determine mount status, aborting." >&2
		exit 1
	fi
}

## Try and determine the device name associated with the root filesystem.
## This is nearly impossible to do in any perfect fashion.
##
## Redhat/Fedora no longer have an rdev command.  Silly them.
## So we now implement it internally, below.
##
## match_rootdev *should* work, but on some distros it may find only "/dev/root",
## and "/dev/root" is not usually a real device.  We leave it like that for now,
## because that's the pattern such systems also use in /proc/mounts.
## Later, at time of use, we'll try harder to find the real rootdev.
##
## FIXME: apparently this doesn't work on SuSE Linux, though.
## So for there, we'll likely need to read /etc/mtab,
## or be a lot more clever and get it somehow from statfs or something.
## FIXME: or use target from /dev/root symlink for Gentoo as well.
##
function match_rootdev() {
	rdev=""
	rdevno="$1"
	while read bdev ; do
		if [ "$rdev" = "" -o "$bdev" != "/dev/root" ]; then
			devno=$($STAT -c "0x%t%02T" "$bdev" 2>/dev/null)
			[ "$devno" = "$rdevno" ] && rdev="$bdev"
		fi
	done
	echo -n "$rdev"
}

rootdev=$($FIND /dev/ -type b 2>/dev/null | match_rootdev $($STAT -c "0x%D" '/'))
[ $verbose -gt 0 ] && echo "rootdev=$rootdev"

## The user gave us a directory (mount point) to TRIM,
## which implies that we will be doing an online TRIM
## using --fallocate and --fibmap to find the free extents.
## Do some preliminary correctness/feasibility checks on fsdir:
##
if [ "$method" = "online" ]; then
	## Ensure fsdir exists and is accessible to us:
	fsdir="$target"
	cd "$fsdir" || exit 1

	if [ "$fsdir" = "/" ]; then
		fsdev="$rootdev"
	else
		## Figure out what device holds the filesystem.
		fsdev="`get_fsdev $fsdir`"
		if [ "$fsdev" = "" ]; then
			echo "$fsdir: not found in /proc/mounts, aborting." >&2
			exit 1
		fi
	fi

	## The root filesystem may show up as the phoney "/dev/root" device
	## in /proc/mounts (ugh).  So if we see that, then substitute the rootdev
	## that $DF gave us earlier.  But $DF may have the same problem (double ugh).
	##
	[ ! -e "$fsdev" -a "$fsdev" = "/dev/root" ] && fsdev="$rootdev"

	## Ensure that fsdev exists and is a block device:
	if [ ! -e "$fsdev" ]; then
		if [ "$fsdev" != "/dev/root" ]; then
			echo "$fsdev: not found" >&2
			exit 1
		fi
		if [ "$rootdev" = "" ]; then
			echo "$fsdev: not found" >&2
			exit 1
		fi
		fsdev="$rootdev"
	fi
	if [ ! -b "$fsdev" ]; then
		echo "$fsdev: not a block device" >&2
		exit 1
	fi

	## If it is mounted read-only, we must switch to doing an "offline" trim of fsdev:
	fsmode="`get_fsmode $fsdir`" || exit 1
	[ $verbose -gt 0 ] && echo "fsmode1: fsmode=$fsmode"
	[ "$fsmode" = "read-only" ] && method=offline
fi

## This is not an "else" clause from the above, because "method" may have changed.
## For offline TRIM, we need the block device, and it cannot be mounted read-write:
##
if [ "$method" = "offline" ]; then
	## We might already have fsdev/fsdir from above; if not, we need to find them.
	if [ "$fsdev" = "" -o "$fsdir" = "" ]; then
		fsdev="$target"
		fsdir="`get_fsdir "$fsdev" < /proc/mounts`"
		## More weirdness for /dev/root in /proc/mounts:
		if [ "$fsdir" = "" -a "$fsdev" = "$rootdev" ]; then
			fsdir="`get_fsdir /dev/root < /proc/mounts`"
			if [ "$fsdir" = "" ]; then
				rdev="`get_devpath /`"
				[ "$rdev" != "" ] && fsdir="`get_fsdir "$rdev" < /proc/mounts`"
			fi
		fi
	fi

	## If the filesystem is truly not-mounted, then fsdir will still be empty here.
	## It could be mounted, though.  Read-only is fine, but read-write means we need
	## to switch gears and do an "online" TRIM instead of an "offline" TRIM.
	##
	if [ "$fsdir" != "" ]; then
		fsmode="`get_fsmode $fsdir`" || exit 1
		[ $verbose -gt 0 ] && echo "fsmode2: fsmode=$fsmode"
		if [ "$fsmode" = "read-write" ]; then
			method=online
			cd "$fsdir" || exit 1
		fi
	fi
fi

## Use $LS to find the major number of a block device:
##
function get_major(){
	$LS -ln "$1" | $GAWK '{print gensub(",","",1,$5)}'
}

## At this point, we have finalized our selection of online vs. offline,
## and we definitely know the fsdev, as well as the fsdir (fsdir="" if not-mounted).
##
## Now guess at the underlying rawdev name, which could be exactly the same as fsdev.
## Then determine whether or not rawdev claims support for TRIM commands.
## Note that some devices lie about support, and later reject the TRIM commands.
##
rawdev=`echo $fsdev | $GAWK '{print gensub("[0-9]*$","","g")}'`
rawdev="`get_realpath "$rawdev"`"
if [ ! -e "$rawdev" ]; then
	rawdev=""
elif [ ! -b "$rawdev" ]; then
	rawdev=""
elif [ "`get_major $fsdev`" -ne "`get_major $rawdev`" ]; then  ## sanity check
	rawdev=""
else
	## "SCSI" drives only; no LVM confusion for now:
	maj="$(get_major $fsdev)"
	maj_ok=0
	for scsi_major in 8 65 66 67 68 69 70 71 ; do
		[ "$maj" = "$scsi_major" ] && maj_ok=1
	done
	if [ $maj_ok -eq 0 ]; then
		echo "$rawdev: does not appear to be a SCSI/SATA SSD, aborting." >&2
		exit 1
	fi
	if ! $HDPARM -I $rawdev | $GREP -i '[ 	][*][ 	]*Data Set Management TRIM supported' &>/dev/null ; then
		if [ "$commit" = "yes" ]; then
			echo "$rawdev: DSM/TRIM command not supported, aborting." >&2
			exit 1
		fi
		echo "$rawdev: DSM/TRIM command not supported (continuing with dry-run)." >&2
	fi
fi
if [ "$rawdev" = "" ]; then
	echo "$fsdev: unable to reliably determine the underlying physical device name, aborting" >&2
	exit 1
fi

## We also need to know the offset of fsdev from the beginning of rawdev,
## because TRIM requires absolute sector numbers within rawdev:
##
fsoffset=`$HDPARM -g "$fsdev" | $GAWK 'END {print $NF}'`

## Next step is to determine what type of filesystem we are dealing with (fstype):
##
if [ "$fsdir" = "" ]; then
	## Not mounted: use $BLKID to determine the fstype of fsdev:
	fstype=`$BLKID -w /dev/null -c /dev/null $fsdev 2>/dev/null | \
		 $GAWK '/ TYPE=".*"/{sub("^.* TYPE=\"",""); sub("[\" ][\" ]*.*$",""); print}'`
	[ $verbose -gt 0 ] && echo "$fsdev: fstype=$fstype"
else
	## Mounted: we could just use $BLKID here, too, but it's safer to use /proc/mounts directly:
	fstype="`$GAWK -v p="$fsdir" '{if ($2 == p) r=$3} END{print r}' < /proc/mounts`"
	[ $verbose -gt 0 ] && echo "$fsdir: fstype=$fstype"
fi
if [ "$fstype" = "" ]; then
	echo "$fsdev: unable to determine filesystem type, aborting." >&2
	exit 1
fi

## Some helper funcs and vars for use with the xfs filesystem tools:
##
function xfs_abort(){
	echo "$fsdev: unable to determine xfs filesystem ${1-parameters}, aborting." >&2
	exit 1
}
function xfs_trimlist(){
	$XFS_DB -r -c "freesp -d" "$fsdev"  ## couldn't get this to work inline
}
xfs_agoffsets=""
xfs_blksects=0

## We used to allow single-drive btrfs here, but it stopped working in linux-2.6.31,
## and Chris Mason says "unsafe at any speed" really.  So it's been dropped now.
##
if [ "$fstype" = "btrfs" ]; then  ## hdparm --fibmap fails, due to fake 0:xx device nodes
	echo "$target: btrfs filesystem type not supported (cannot determine physical devices), aborting." >&2
	exit 1
fi

## Now figure out whether we can actually do TRIM on this type of filesystem:
##
if [ "$method" = "online" ]; then
	## Print sensible error messages for some common situations,
	## rather than failing with more confusing messages later on..
	##
	if [ "$fstype" = "ext2" -o "$fstype" = "ext3" ]; then  ## No --fallocate support
		echo "$target: cannot TRIM $fstype filesystem when mounted read-write, aborting." >&2
		exit 1
	fi

	## Figure out if we have enough free space to even attempt TRIM:
	##
	freesize=`$DF -P -B 1024 . | $GAWK '{r=$4}END{print r}'`
	if [ "$freesize" = "" ]; then
		echo "$fsdev: unknown to '$DF'"
		exit 1
	fi
	if [ $freesize -lt 15000 ]; then
		echo "$target: filesystem too full for TRIM, aborting." >&2
		exit 1
	fi

	## Figure out how much space to --fallocate (later), keeping in mind
	## that this is a live filesystem, and we need to leave some space for
	## other concurrent activities, as well as for filesystem overhead (metadata).
	## So, reserve at least 1% or 7500 KB, whichever is larger:
	##
	reserved=$((freesize / 100))
	[ $reserved -lt 7500 ] && reserved=7500
	[ $verbose -gt 0 ] && echo "freesize = ${freesize} KB, reserved = ${reserved} KB"
	tmpsize=$((freesize - reserved))
	tmpfile="WIPER_TMPFILE.$$"
	get_trimlist="$HDPARM --fibmap $tmpfile"
else
	## We can only do offline TRIM on filesystems that we "know" about here.
	## Currently, this includes the ext2/3/4 family, xfs, and reiserfs.
	## The first step for any of these is to ensure that the filesystem is "clean",
	## and immediately abort if it is not.
	##
	get_trimlist=""
	if [ "$fstype" = "ext2" -o "$fstype" = "ext3" -o "$fstype" = "ext4" ]; then
		DUMPE2FS=`find_prog /sbin/dumpe2fs` || exit 1
		fstate="`$DUMPE2FS $fsdev 2>/dev/null | $GAWK '/^[Ff]ilesystem state:/{print $NF}' 2>/dev/null`"
		if [ "$fstate" != "clean" ]; then
			echo "$target: filesystem not clean, please run \"e2fsck $fsdev\" first, aborting." >&2
			exit 1
		fi
		get_trimlist="$DUMPE2FS $fsdev"
	elif [ "$fstype" = "xfs" ]; then
		XFS_DB=`find_prog /sbin/xfs_db` || exit 1
		XFS_REPAIR=`find_prog /sbin/xfs_repair` || exit 1
		if ! $XFS_REPAIR -n "$fsdev" &>/dev/null ; then
			echo "$fsdev: filesystem not clean, please run \"xfs_repair $fsdev\" first, aborting." >&2
			exit 1
		fi

		## For xfs, life is more complex than with ext2/3/4 above.
		## The $XFS_DB tool does not return absolute block numbers for freespace,
		## but rather gives them as relative to it's allocation groups (ag's).
		## So, we'll need to interogate it for the offset of each ag within the filesystem.
		## The agoffsets are extracted from $XFS_DB as sector offsets within the fsdev.
		##
		agcount=`$XFS_DB -r -c "sb" -c "print agcount" "$fsdev" | $GAWK '{print 0 + $NF}'`
		[ "$agcount" = "" -o "$agcount" = "0" ] && xfs_abort "agcount"
		xfs_agoffsets=
		i=0
		while [ $i -lt $agcount ]; do
			agoffset=`$XFS_DB -r -c "sb" -c "convert agno $i daddr" "$fsdev" \
				| $GAWK '{print 0 + gensub("[( )]","","g",$2)}'`
			[ "$agoffset" = "" ] && xfs_abort "agoffset-$i"
			[ $i -gt 0 ] && [ $agoffset -le ${xfs_agoffsets##* } ] && xfs_abort "agoffset[$i]"
			xfs_agoffsets="$xfs_agoffsets $agoffset"
			i=$((i + 1))
		done
		xfs_agoffsets="${xfs_agoffsets:1}"	## strip leading space

		## We also need xfs_blksects for later, because freespace gets listed as block numbers.
		##
		blksize=`$XFS_DB -r -c "sb" -c "print blocksize" "$fsdev" | $GAWK '{print 0 + $NF}'`
		[ "$blksize" = "" -o "$blksize" = "0" ] && xfs_abort "block size"
		xfs_blksects=$((blksize/512))
		get_trimlist="xfs_trimlist"
	elif [ "$fstype" = "reiserfs" ]; then
		DEBUGREISERFS=`find_prog /sbin/debugreiserfs` || exit 1
		( $DEBUGREISERFS $fsdev | $GREP '^Filesystem state:.consistent' ) &> /dev/null
		if [ $? -ne 0 ]; then
			echo "Please run fsck.reiserfs first, aborting." >&2
			exit 1
		fi
		get_trimlist="$DEBUGREISERFS -m $fsdev"
	elif [ "$fstype" = "hfsplus" ]; then
		OD=`find_prog /usr/bin/od` || exit 1
		TR=`find_prog /usr/bin/tr` || exit 1
		#check sleuthkit
		FSSTAT=`find_prog /usr/local/bin/fsstat` 
		if [ "$?" = "1" ]; then
			echo "fsstat and icat from package sleuthkit >= 3.1.1 is required for hfsplus."
			exit 1
		fi
		ICAT=`find_prog /usr/local/bin/icat` 
		if [ "`$ICAT -f list 2>/dev/stdout|$GREP HFS+`" = "" ]; then
                        echo "Wrong icat, version from package sleuthkit >= 3.1.1 is required for hfsplus."
                        exit 1
                fi
		#check for unmounted properly
		if [ "`$FSSTAT -f hfs $fsdev | $GREP "Volume Unmounted Properly"`" = ""  ]; then
			echo "Hfsplus volume unmounted improperly!"
			exit 1
		fi
		#check $AllocationFile inode
		FFIND=`find_prog /usr/local/bin/ffind`
		if [ "`$FFIND -f hfs $fsdev 6`" != "/\$AllocationFile" ]; then
			echo "Hfsplus bitmap \$AllocationFile is not inode 6!"
			exit 1
		fi
		#get offset for hfsplus with a wrapper
		hfsoffset=`$FSSTAT -f hfs $fsdev | $GREP "File system is embedded in an HFS wrapper at offset "|$TR -d "\t"`
		if [ -n "$hfsoffset" ]; then
			hfsoffset=${hfsoffset:52}
			((fsoffset=fsoffset+hfsoffset))
			echo "File system is embedded in an HFS wrapper at offset $hfsoffset"
		fi
		blksize=`$FSSTAT -f hfs $fsdev | $GREP "Allocation Block Size: "|$TR -d "\t"`
		blksize=${blksize:23}
		blksects=$((blksize / 512))
		#get count of used bytes in $AllocationFile
		blkcount=`$FSSTAT -f hfs $fsdev | $GREP "Block Range: 0 - "`
		blkcount=${blkcount:17}
		bytecount=$((blkcount/blksects))
		
		method="bitmap_offline"
		get_trimlist="echo $blksects hfsplus `$ICAT -f hfs $fsdev 6 | $OD -N $bytecount -An -vtu1 -j0 -w1`"
	elif [ "$fstype" = "ntfs" ]; then
		NTFSINFO=`find_prog /usr/bin/ntfsinfo` || exit 1
		NTFSCAT=`find_prog /usr/bin/ntfscat` || exit 1
		NTFSPROBE=`find_prog /usr/bin/ntfs-3g.probe` || exit 1
		OD=`find_prog /usr/bin/od` || exit 1
		TR=`find_prog /usr/bin/tr` || exit 1
		#check for unmounted properly
		$NTFSPROBE -w $fsdev 2>/dev/null
		if [ $? -ne 0 ]; then
			echo "$fsdev contains an unclean file system!"
			exit 1
		fi
		#check for volume version
		if [ "`$NTFSINFO -m -f $fsdev | $GREP "Volume Version: 3.1"`" = "" ]; then
			echo "NTFS volume version must be 3.1!"
			exit 1
		fi
		blksize=`$NTFSINFO -m -f $fsdev | $GREP "Cluster Size: " | $TR -d "\t"`
		blksize=${blksize:14}
		blksects=$((blksize / 512))
		#get count of used bytes in $Bitmap
		blkcount=`$NTFSINFO -m -f $fsdev | $GREP "Volume Size in Clusters: " | $TR -d "\t"`
		blkcount=${blkcount:25}
		bytecount=$((blkcount/blksects))

		method="bitmap_offline"
		get_trimlist="echo $blksects ntfs `$NTFSCAT $fsdev \\\$Bitmap | $OD -N $bytecount -An -vtu1 -j0 -w1`"
	fi
	if [ "$get_trimlist" = "" ]; then
		echo "$target: offline TRIM not supported for $fstype filesystems, aborting." >&2
		exit 1
	fi
fi

## All ready.  Now let the user know exactly what we intend to do:
##
mountstatus="$fstype non-mounted"
[ "$fsdir" = "" ] || mountstatus="$fstype mounted $fsmode at $fsdir"
echo "Preparing for $method TRIM of free space on $fsdev ($mountstatus)."

## If they specified "--commit" on the command line, then prompt for confirmation first:
##
if [ "$commit" = "yes" ]; then
	if [ "$destroy_me" = "" ]; then
		echo >/dev/tty
		echo -n "This operation could silently destroy your data.  Are you sure (y/N)? " >/dev/tty
		read yn < /dev/tty
		if [ "$yn" != "y" -a "$yn" != "Y" ]; then
			echo "Aborting." >&2
			exit 1
		fi
	fi
	TRIM="$HDPARM --please-destroy-my-drive --trim-sector-ranges-stdin $rawdev"
else
	echo "This will be a DRY-RUN only.  Use --commit to do it for real."
	TRIM="$GAWK {}"
fi

## Useful in a few places later on:
##
function sync_disks(){
	echo -n "Syncing disks.. "
	sync
	echo
}

## Clean up tmpfile (if any) and exit:
##
function do_cleanup(){
	if [ "$method" = "online" ]; then
		if [ -e $tmpfile ]; then
			echo "Removing temporary file.."
			$RM -f $tmpfile
		fi
		sync_disks
	fi
	[ $1 -eq 0 ] && echo "Done."
	[ $1 -eq 0 ] || echo "Aborted." >&2
	exit $1
}

## Prepare signal handling, in case we get interrupted while $tmpfile exists:
##
function do_abort(){
	echo
	do_cleanup 1
}
trap do_abort SIGTERM
trap do_abort SIGQUIT
trap do_abort SIGINT
trap do_abort SIGHUP
trap do_abort SIGPIPE

## For online TRIM, go ahead and create the huge temporary file.
## This is where we finally discover whether the filesystem actually
## supports --fallocate or not.  Some folks will be disappointed here.
##
## Note that --fallocate does not actually write any file data to fsdev,
## but rather simply allocates formerly-free space to the tmpfile.
##
if [ "$method" = "online" ]; then
	if [ -e "$tmpfile" ]; then
		if ! $RM -f "$tmpfile" ; then
			echo "$tmpfile: already exists and could not be removed, aborting." >&2
			exit 1
		fi
	fi
	echo -n "Creating temporary file (${tmpsize} KB).. "
	if ! $HDPARM --fallocate "${tmpsize}" $tmpfile ; then
		echo "$target: this kernel may not support 'fallocate' on a $fstype filesystem, aborting." >&2
		exit 1
	fi
	echo
fi

## Finally, we are now ready to TRIM something!
##
## Feed the "get_trimlist" output into a gawk program which will
## extract the trimable lba-ranges (extents) and batch them together
## into huge --trim-sector-ranges calls.
##
## We are limited by at least one thing when doing this:
##   1. Some device drivers may not support more than 255 sectors
##      full of lba:count range data per TRIM command.
## The latest hdparm versions now take care of that automatically.
##
sync_disks
if [ "$commit" = "yes" ]; then
	echo "Beginning TRIM operations.."
else
	echo "Simulating TRIM operations.."
fi
[ $verbose -gt 0 ] && echo "get_trimlist=$get_trimlist"

## Begin gawk program
GAWKPROG='
	BEGIN {
		if (xfs_agoffsets != "") {
			method = "xfs_offline"
			agcount = split(xfs_agoffsets,agoffset," ");
		}
	}
	function append_range (lba,count  ,this_count){
		nsectors += count;
		while (count > 0) {
			this_count  = (count > 65535) ? 65535 : count
			printf "%u:%u ", lba, this_count
			if (verbose > 1)
				printf "%u:%u ", lba, this_count > "/dev/stderr"
			lba        += this_count
			count      -= this_count
			nranges++;
		}
	}
	(method == "online") {	## Output from "hdparm --fibmap", in absolute sectors:
		if (NF == 4 && $2 ~ "^[1-9][0-9]*$")
			append_range($2,$4)
		next
	}
	(method == "xfs_offline") { ## Output from xfs_db:
		if (NF == 3 && gensub("[0-9 ]","","g",$0) == "" && $1 < agcount) {
			lba   = agoffset[1 + $1] + ($2 * xfs_blksects) + fsoffset
			count = $3 * xfs_blksects
			append_range(lba,count)
		}
		next
	}
	(method == "bitmap_offline") {
		n = split($0,f)
		blksects = f[1]
		fstype = f[2]
		bitmap_start = 3
		range_first = -1 #clusters
		range_last = -1
		for (i = bitmap_start; i <= n-1; i++) {
			if (f[i] == 0) {
				if (range_first == -1)
					range_first = (i-bitmap_start) * 8 
				range_last = (i-bitmap_start) * 8 + 7
			} else if (f[i] == 255 && range_first > -1){
				#printf range_first "-" range_last "\n" > "/dev/stderr"
				lba = (range_first * blksects) + fsoffset
				count = (range_last - range_first + 1) * blksects
				append_range(lba,count)
				range_first = -1
				range_last = -1
			} else {
				for (b = 0; b < 8; b++) {
					if (fstype == "ntfs")
						bit = and(f[i], lshift(1, b)) ? 1 : 0
					else #hfsplus
						bit = and(f[i], lshift(1, 7-b)) ? 1 : 0
					if (bit == 0) {
						if (range_first == -1) {
							range_first = (i-bitmap_start) * 8 + b
							range_last = (i-bitmap_start) * 8 + b
						} else
							range_last += 1
					} else if (range_first > -1) {
						#printf range_first "-" range_last " " > "/dev/stderr"
						lba = (range_first * blksects) + fsoffset
						count = (range_last - range_first + 1) * blksects
						if (fstype == "ntfs")
							append_range(lba,count)
						else if (count > (2 * blksects)) #faster for hfsplus
							append_range(lba,count)
						range_first = -1
						range_last = -1
					}
				}
			}
		}
		if (range_first > -1){
			#printf range_first "-" range_last " " > "/dev/stderr"
			lba = (range_first * blksects) + fsoffset
			count = (range_last - range_first + 1) * blksects
			append_range(lba,count)
		}
		next
	}
	/^Block size: *[1-9]/ {	## First stage output from dumpe2fs:
		blksects = $NF / 512
		next
	}
	/^Group [0-9][0-9]*:/ {	## Second stage output from dumpe2fs:
		in_groups = 1
		next
	}
	/^ *Free blocks: [0-9]/	{ ## Bulk of output from dumpe2fs:
		if (blksects && in_groups) {
			n = split(substr($0,16),f,",*  *")
			for (i = 1; i <= n; ++i) {
				if (f[i] ~ "^[1-9][0-9]*-[1-9][0-9]*$") {
					split(f[i],b,"-")
					lba   = (b[1] * blksects) + fsoffset
					count = (b[2] - b[1] + 1) * blksects
					append_range(lba,count)
				} else if (f[i] ~ "^[1-9][0-9]*$") {
					lba   = (f[i] * blksects) + fsoffset
					count = blksects
					append_range(lba,count)
				}
			}
			next
		}
	}
	/^Reiserfs super block/ {
		method = "reiserfs"
		next
	}
	/^Blocksize: / {
		if (method == "reiserfs") {
			blksects = $2 / 512
			next
		}
	}
	/^#[0-9][0-9]*:.*Free[(]/ { ## debugreiserfs
		if (method == "reiserfs" && blksects > 0) {
			n = split($0,f)
			for (i = 4; i <= n; ++i) {
				if (f[i] ~ "^ *Free[(]") {
					if (2 == split(gensub("[^-0-9]","","g",f[i]),b,"-")) {
						lba = (b[1] * blksects) + fsoffset
						count = (b[2] - b[1] + 1) * blksects
						append_range(lba, count)
					}
				}
			}
			next
		}
	}
	END {
		if (err == 0 && commit != "yes")
			printf "(dry-run) trimming %u sectors from %u ranges\n", nsectors, nranges > "/dev/stderr"
		exit err
	}'
## End gawk program

$get_trimlist 2>/dev/null | $GAWK		\
	-v commit="$commit"			\
	-v method="$method"			\
	-v rawdev="$rawdev"			\
	-v fsoffset="$fsoffset"			\
	-v verbose="$verbose"			\
	-v xfs_blksects="$xfs_blksects"		\
	-v xfs_agoffsets="$xfs_agoffsets"	\
	"$GAWKPROG" | $TRIM

do_cleanup $?
