diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..79d27c5 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +fatmapper is written and maintained by Patrick Neumann . diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..00b00ad --- /dev/null +++ b/CHANGES @@ -0,0 +1,21 @@ +Release 1.0 (in development) +============================ + + +(Incompatible) changes +---------------------- + +* none + + +Features added +-------------- + +* none + + +Bugs fixed +---------- + +* none + diff --git a/doc/fatmapper_documentation.pdf b/doc/fatmapper_documentation.pdf new file mode 100644 index 0000000..ab63946 Binary files /dev/null and b/doc/fatmapper_documentation.pdf differ diff --git a/fatmapper.py b/fatmapper.py new file mode 100755 index 0000000..58b4275 --- /dev/null +++ b/fatmapper.py @@ -0,0 +1,858 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +"""Display general details of a FAT file system.""" + +#=============================================================================== +# +# FILE: +# ./fatmapper.py +# +# BASIC USAGE: +# $ ./fatmapper.py [-h|--help] [-o|--offset OFFSET] image +# OR +# $ python fatmapper.py [-h|--help] [-o|--offset OFFSET] image +# +# OPTIONS: +# -h, +# --help show help message and exit +# -o OFFSET, +# --offset OFFSET offset in sectors (default=0) +# image raw image file (esp. dd) +# +# EXIT STATES: +# 0 = success +# 1 = Python version not tested/supported +# 2 = image file does not exist +# 3 = offset is not a positive integer +# 4 = wrong or missing data in dictionary +# 5 = empty image file +# 6 = image file smaller than offset +# 7 = invalid vbr signatures +# +# REQUIREMENTS: +# python2.7 +# +# NOTES: +# Tested on: +# - ArchLinux (64-Bit) + python 2.7.13 +# - Raspbian GNU/Linux 8.0 (32-Bit) + python 2.7.9 +# - macOS (10.12.3) + python 2.7.10 +# - Bash on Ubuntu on Windows 10 (64-Bit) + python 2.7.6 +# with: +# - raw images +# - filesystem(s) in partition(s) +# - filesystem without a partition +# - no partition/no filesystem (empty file) +# - invalid filesystem +# - FAT12, FAT16 and FAT32 +# +# WARRANTY: +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# See the LICENSE file for more details. +# +# HISTORY: +# See the CHANGES file for more details. +# +#=============================================================================== + +#=== MODULES =================================================================== + +import argparse +import hashlib +import os +import os.path +import re +import struct +import sys + +#=== INFO ====================================================================== + +"""@author: Patrick Neumann +@contact: patrick@neumannsland.de +@copyright: Copyright (C) 2017, Patrick Neumann +@license: GNU General Public License 3.0 +@date: 2017-03-26 +@version: 1.0.0 +@status: Development +""" + +#=== CHECKS ==================================================================== + +"""Only tested with Python versions 2.7.9, 2.7.10 and 2.7.13! + +Python 3.x is not supported! +""" + +if sys.version_info < (2, 7, 0): + sys.stderr.write("Sorry, your Python version was not tested but may work?\n") + sys.exit(1) + +if sys.version_info >= (3, 0, 0): + sys.stderr.write("Error: Python 3.x is not supported!\n") + sys.exit(1) + +#=== FUNCTIONS ================================================================= + +# This is a private module function only for the other module functions. +# Unfortunately, the underscore does not prevent other developers from using +# it directly from outside this module. :-( +# sphinx-apidoc does ignore it. :-) +def _checkimage(_file): + """Check image file. + + Check if the image is a existing file. + + Parameters + ---------- + _file : str + path to the image file. + + Raises + ------ + Exits if the image does not exist. + """ + + if not os.path.isfile(_file): + sys.stderr.write("Error: image file does not exist!\n") + sys.exit(2) + +#------------------------------------------------------------------------------- + +# This is a private module function only for the other module functions. +# Unfortunately, the underscore does not prevent other developers from using +# it directly from outside this module. :-( +# sphinx-apidoc does ignore it. :-) +def _checkoffset(loffset): + """Check offset. + + Check if the offset is an integer and is equal or greater than zero. + + Parameters + ---------- + loffset : int + offset in bytes in the image file. + + Raises + ------ + Exits if the offset is not equal or greater than zero. + """ + + if not isinstance(loffset, int): + sys.stderr.write("Error: offset is not an integer!\n") + sys.exit(3) + + if not loffset >= 0: + sys.stderr.write("Error: offset is not equal or greater zero!\n") + sys.exit(3) + +#------------------------------------------------------------------------------- + +# This is a private module function only for the other module functions. +# Unfortunately, the underscore does not prevent other developers from using +# it directly from outside this module. :-( +# sphinx-apidoc does ignore it. :-) +def _checkdict(check, _dict): + """Check dictionary. + + Check if the dictionary contains all needed data and that the data has + the right type. + + Parameters + ---------- + check : dict) + dictionary with necessary keys and types of the data dictionary. + + _dict : dict + dictionary with mapping of parts of a fat VBR to a human readable index. + + Raises + ------ + Exits if the data dictionary does not contain the necessary keys or the + type of data is incorrect. + """ + + for key, var in check.items(): + if not key in _dict: + sys.stderr.write("Error: necessary data is missing in dictionary!\n") + sys.exit(4) + # attention: on 32-Bit systems there is more often used long + # where on 64-Bit systems is int big enough: + if not isinstance(_dict[key], var): + sys.stderr.write("Error: invalid type of data in dictionary!\n") + sys.exit(4) + +#------------------------------------------------------------------------------- + +# This is a private module function only for the other module functions. +# Unfortunately, the underscore does not prevent other developers from using +# it directly from outside this module. :-( +# sphinx-apidoc does ignore it. :-) +def _getargs(): + """Get command line arguments. + + Parses the command line arguments, checks and return them. + + Returns + ------- + tuple + A tuple containing the checked path to the image file + and the offset in bytes in the image file calculated from sectors. + + The default offset is 0. + + For example: + ( "fat12_in_partition.dd", 1048576 ) + """ + + parser = argparse.ArgumentParser() + parser.add_argument("image", + type=str, + help="raw image file (esp.: image.dd)") + parser.add_argument("-o", + "--offset", + type=int, + default=0, + help="offset in sectors (default=0)") + args = parser.parse_args() + + _checkimage(args.image) + limage = args.image + + _checkoffset(args.offset) + loffset = args.offset * 512 + + return (limage, loffset) + +#------------------------------------------------------------------------------- + +def getsize(_file, loffset): + """Get image file size. + + Get size of the image file. + + Parameters + ---------- + _file : str + path to the image file. + + loffset : int + offset in bytes in the image file. + + Returns + ------- + int + size of image file in bytes. + + Raises + ------ + Exits if image file is empty or smaller than offset. + """ + + _checkimage(_file) + _checkoffset(loffset) + + lsize = os.stat(_file)[6] + + if lsize == 0: + sys.stderr.write("Error: the image file is an empty file!\n") + sys.exit(5) + + if lsize < loffset + 512: + sys.stderr.write("Error: size of the image file is smaller than offset (+ 512 byte)!\n") + sys.exit(6) + + return lsize + +#------------------------------------------------------------------------------- + +def getmd5(_file): + """Get MD5 of image file. + + Get MD5 digest in hex for the content of the image file. + + Parameters + ---------- + _file : str + path to the image file. + + Returns + ------- + str + MD5 digest in uppercase hex. + """ + + _checkimage(_file) + + lfile = open(_file, "rb") + lmd5 = hashlib.md5(lfile.read()).hexdigest().upper() + lfile.close() + + return lmd5 + +#------------------------------------------------------------------------------- + +def checkvbr(_file, loffset): + """Check for FAT VBR. + + Check for the existence of a valid FAT12/16/32 VBR. + + Parameters + ---------- + _file : str + path to the image file. + + loffset : int + offset in bytes in the image file. + + Returns + ------- + str + file_system_type (FAT1X or FAT32) + + Raises + ------ + Exits if the VBR can not be verified as a FAT12/16/32 VBR. + """ + + _checkimage(_file) + _checkoffset(loffset) + + lfile = open(_file, "rb") + lfile.seek(loffset + 38) + extended_signature = lfile.read(1) + + if extended_signature == "\x29": + lfile.seek(15, 1) + file_system_type = lfile.read(8) + lfile.seek(448, 1) + vbr_signature = lfile.read(2) + file_system_type_switch = "FAT1X" + else: + lfile.seek(loffset + 66) + extended_signature = lfile.read(1) + lfile.seek(15, 1) + file_system_type = lfile.read(8) + lfile.seek(420, 1) + vbr_signature = lfile.read(2) + file_system_type_switch = "FAT32" + lfile.close() + + condition1 = extended_signature == "\x29" + condition2 = re.search(r'FAT(1(2|6)|32) ', file_system_type) + condition3 = vbr_signature == "\x55\xaa" + + if not (condition1 and condition2 and condition3): + sys.stderr.write("Error: no valid FAT12/FAT16/FAT32 VBR found!\n") + sys.exit(7) + + #print "fileSystemTypeSwitch:", fileSystemTypeSwitch + + return file_system_type_switch + +#------------------------------------------------------------------------------- + +def getraw(_file, loffset): + """Get global data. + + Read raw data from the VBR into a python dictionary. + + Parameters + ---------- + _file : str + path to the image file. + + loffset : int + offset in bytes in the image file. + + Returns + ------- + dict + dictionary with mapping of parts of a fat VBR to a human readable index. + + For example: + + {"bytesPerSector": 512, + "sectorsPerCluster": 64, + "reservedSectors": 1, + "fatCopies": 2, + "rootDirectoryEntries": 1024, + "sectorsInPartition": 0, + "rawMediaDescriptor": 248, + "sectorsPerFat": 64, + "sectorsPerTrack": 32, + "numberOfHeads": 64, + "hiddenSectors": 0, + "largerSectorsInPartition": 260096} + """ + + _checkimage(_file) + _checkoffset(loffset) + + keys = ["bytesPerSector", "sectorsPerCluster", "reservedSectors", + "fatCopies", "rootDirectoryEntries", "sectorsInPartition", + "rawMediaDescriptor", "sectorsPerFat", "sectorsPerTrack", + "numberOfHeads", "hiddenSectors", "largerSectorsInPartition"] + + lfile = open(_file, "rb") + lfile.seek(loffset + 3) # just skip the jumpcode + oem_name_version = lfile.read(8) + raw = lfile.read(2 + 1 + 2 + 1 + 2 + 2 + 1 + 2 + 2 + 2 + 4 + 4) + values = struct.unpack_from("= 2 and cluster <= 268435446: + if cluster > _max: + _max = cluster + if cluster < _min: + _min = cluster + counter = counter + 1 + _next = loffset + (_dict["reservedSectors"] * _dict["bytesPerSector"]) + (cluster * 4) + if not lfile.tell() == _next: + lfile.seek(_next - lfile.tell(), 1) + raw = lfile.read(4) + cluster = struct.unpack_from("