#!/usr/bin/env python3 # -*- coding: utf-8 -*- """List file and directory names in a FAT16 file system (disk image).""" #=============================================================================== # # FILE: # ./fatlister.py # # BASIC USAGE: # $ ./fatlister.py [-h|--help] [-o|--offset OFFSET] [-f|--file FILENAME] IMAGE # OR # $ python3 fatlister.py [-h|--help] [-o|--offset OFFSET] [-f|--file FILENAME] IMAGE # # OPTIONS: # -h, # --help show help message and exit # -o OFFSET, # --offset OFFSET offset in sectors (default=0) # -f FILENAME, write output to file (default=unset) # --file FILENAME if unset output goes to stdout # IMAGE raw image file (esp. dd) # # EXIT STATES: # 0 = success # 1 = Python version not tested/supported # 2 = states thrown by argparse: # - IMAGE not passed to fatlister # - unknown argument passed to fatlister # - OFFSET is not an integer # 3 = image file does not exist # 4 = file is not readable # 5 = offset is not a positive integer # 6 = output file already exist # 7 = output directory is not writable # 8 = empty image file # 9 = image file smaller than offset # 10 = invalid vbr signatures # # REQUIREMENTS: # python3.4+ # # NOTES: # Tested on: # - ArchLinux (64-Bit) + python 3.6.1 # - Raspbian GNU/Linux 8.0 (32-Bit) + python 3.4.2 # - macOS (10.12.4) + python 3.6.1 (from Homebrew) # - Bash on Ubuntu on Windows 10 "1703" (64-Bit) + python 3.5.2 # with: # - raw images # - filesystem(s) in partition(s) <--- TODO! # - filesystem without a partition # - no partition/no filesystem (empty file) # - invalid filesystem # - FAT16 # # 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 os import os.path import re import struct import sys import time #=== 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.1 @status: Development """ #=== CHECKS ==================================================================== """Only tested with Python versions 3.4.x - 3.6.x!""" if sys.version.startswith("2"): sys.stderr.write("Python version 2.x is not supportet!\n") sys.exit(1) if sys.version_info < (3, 4, 0): sys.stderr.write("Sorry, your Python version was not tested but may work?\n") sys.exit(1) #=== CLASS ===================================================================== class Fatlister: def __init__(self, image, offset): self._checkimage(image) self.image = image self._checkoffset(offset) self.offset = offset self._checkvbr() self.output = ['# This RFC4180 compliant file was created with fatlister.py at ' + time.strftime("%Y-%m-%dT%H:%M:%S") + '.', '"id", "path", "name", "LFN", "Allocated", "Size", "Accessed", "Modified", "Created", "Starting Cluster"'] self._processvbr() self._readfiles() #------------------------------------------------------------------------------- # This is a private method for usage in this class only. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this class. :-( # sphinx-apidoc does ignore it. :-) def _checkimage(self, _file): """Check image file. Check if the image is a existing and readable file. Parameters ---------- _file : str path to the image file. Raises ------ Exits if the image does not exist or is not readable. """ if not os.path.isfile(_file): sys.stderr.write("Error: image file does not exist!\n") sys.exit(3) size = os.stat(_file)[6] if size == 0: sys.stderr.write("Error: the image file is an empty file!\n") sys.exit(8) # Check if image is readable the first time if we need to read it! #------------------------------------------------------------------------------- # This is a private method for usage in this class only. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this class. :-( # sphinx-apidoc does ignore it. :-) def _checkoffset(self, offset): """Check offset. Check if the offset is an integer and is equal or greater than zero. Parameters ---------- offset : int offset in bytes in the image file. Raises ------ Exits if the offset is not equal or greater than zero. """ if not offset >= 0: sys.stderr.write("Error: offset is not equal or greater zero!\n") sys.exit(5) size = os.stat(self.image)[6] if size < offset + 512: sys.stderr.write("Error: size of the image file is smaller than offset (+ 512 byte)!\n") sys.exit(9) #------------------------------------------------------------------------------- # This is a private method for usage in this class only. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this class. :-( # sphinx-apidoc does ignore it. :-) def _checkvbr(self): """Check for FAT VBR. Check for the existence of a valid FAT12/16 VBR. Parameters ---------- self.image : str path to the image file. self.offset : int offset in bytes in the image file. Raises ------ Exits if the VBR can not be verified as a FAT12/16 VBR. """ try: with open(self.image, "rb") as isreadable: isreadable.seek(self.offset + 38) extended_signature = isreadable.read(1) isreadable.seek(15, 1) file_system_type = isreadable.read(8) isreadable.seek(448, 1) vbr_signature = isreadable.read(2) except IOError: sys.stderr.write("Error: image file is not readable!\n") sys.exit(4) condition1 = extended_signature == b"\x29" condition2 = re.search(r'FAT1(2|6) ', file_system_type.decode("ascii")) condition3 = vbr_signature == b"\x55\xaa" if not (condition1 and condition2 and condition3): sys.stderr.write("Error: no valid FAT12/FAT16 VBR found!\n") sys.exit(10) #------------------------------------------------------------------------------- def printoutput(self): """Print to stdout. Prints every item of self.output to stdout. Parameters ---------- self.output : list List with every line of output as a item. """ for line in self.output: print(line) #------------------------------------------------------------------------------- def writeoutput(self): """Write to file. Writes every item of self.output to a file. Parameters ---------- self.output : list List with every line of output as a item. Raises ------ Exits if output directory is not writable. """ try: with open(self.file, "w") as iswritable: for line in self.output: iswritable.write(line + "\n") except IOError: sys.stderr.write("Error: output directory is not writable!\n") sys.exit(7) #------------------------------------------------------------------------------- # This is a private method for usage in this class only. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this class. :-( # sphinx-apidoc does ignore it. :-) def _processvbr(self): """Process FAT12/FAT16 VBR. Read, translate and calculate needed information out of the vbr and write it into single attributes. """ # We have already checked if we are able to read the image. with open(self.image, "rb") as image: image.seek(self.offset + 11) raw = image.read(2 + 1 + 2 + 1 + 2) parts = struct.unpack_from("> 9) month = "{:02}".format((0b0000000111100000 & unsignedShort) >> 5) day = "{:02}".format(0b0000000000011111 & unsignedShort) return (year, month, day) #------------------------------------------------------------------------------- def _calculatetime(unsignedShort): # TODO: # candidate for external use? # add test for unignedShort # add comment hours = "{:02}".format((0b1111100000000000 & unsignedShort) >> 11) minutes = "{:02}".format((0b0000011111100000 & unsignedShort) >> 5) seconds = "{:02}".format((0b0000000000011111 & unsignedShort) * 2) return (hours, minutes, seconds) #------------------------------------------------------------------------------- # This is a private method for usage in this class only. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this class. :-( # sphinx-apidoc does ignore it. :-) def _readfiles(self): """Loop over directories and files. Loop over all directories and add each file with SFN and optionally LFN to the list (self.outout). TODO ---- 1. Move subloops to separate methods!? """ directoryOffsets = [self.offset + self.firstRootDirectoryEntryLocation] directories = {self.offset + self.firstRootDirectoryEntryLocation: "/"} _id = 0 longFileName = "" # We have already checked if we are able to read the image. with open(self.image, "rb") as image: for directoryOffset in directoryOffsets: fileOffset = directoryOffset for entry in range(int(self.bytesPerCluster / 32)): image.seek(fileOffset) filename = image.read(8) # skip . or ..: if re.search(b"^\x2e\x20{7}|\x2e{2}\x20{6}", filename): fileOffset = fileOffset + 32 continue # if unused: elif re.search(b"^\x00", filename): break # if deleted: elif re.search(b"^\xe5", filename): filename = b"\x3f" + filename[1:] # deleted (sigma) to ? allocated = "deleted" else: # if the filename starts with "sigma": if re.search(b"^\x05", filename): filename = b"\xe5" + filename[1:] allocated = "allocated" filenameExtension = image.read(3) fileAttributes = struct.unpack_from("H", struct.unpack_from("H", struct.unpack_from("H", struct.unpack_from(" 65526: break fileOffset = fileOffset + 32 continue fileSize = parts[3] self.output.append('"' + str(_id) + '", ' \ + '"' + directories[directoryOffset] + '", ' \ + '"' + filename + '", ' \ + '"' + longFileName + '", ' \ + '"' + allocated + '", ' \ + '"' + str(fileSize) + '", ' \ + '"' + str(accessedYear) + '-' + str(accessedMonth) + '-' + str(accessedDay) + '", ' \ + '"' + str(modifiedYear) + '-' + str(modifiedMonth) + '-' + str(modifiedDay) + 'T' + str(modifiedHours) + ':' + str(modifiedMinutes) + ':' + str(modifiedSeconds) + '", ' \ + '"' + str(createdYear) + '-' + str(createdMonth) + '-' + str(createdDay) + 'T' + str(createdHours) + ':' + str(createdMinutes) + ':' + str(createdSeconds) + '", ' \ + '"' + str(startingCluster) + '"') # if we don't have a LFN: if not fileAttributes == 15: longFileName = "" _id = _id + 1 fileOffset = fileOffset + 32 #=== 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 _getargs(): """Get command line arguments. Parses the command line arguments and return them. (Don't forget to check them in the next steps?) Returns ------- tuple A tuple containing the to the image file, the offset in bytes in the image file calculated from sectors and the output file. The default offset is 0. For example: ( "image.dd", 2048, "output.csv" ) Raises ------ Exits if argparse detects an error. """ 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)") parser.add_argument("-f", "--file", type=str, default="", help='output file (default="")') args = parser.parse_args() return (args.image, args.offset * 512, args.file) #------------------------------------------------------------------------------- # 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 class. :-( # sphinx-apidoc does ignore it. :-) def _checkfile(_file): """Check output file. Check if the output file is not existing and the target directory is writable. Parameters ---------- _file : str path to the output file. Raises ------ Exits if the image does not exist or is not readable. """ if os.path.isfile(_file): sys.stderr.write("Error: output file already exist!\n") sys.exit(6) # Check write permissions if we try to write to the file the first time! #=== MAIN ====================================================================== if __name__ == '__main__': (IMAGE, OFFSET, FILE) = _getargs() OBJECT = Fatlister(IMAGE, OFFSET) if not FILE: OBJECT.printoutput() else: _checkfile(FILE) OBJECT.file = FILE OBJECT.writeoutput() sys.exit(0)