Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- """Examines a FAT12 or FAT16 volume image.
- NAME
- lookfat
- SYNOPSIS
- lookfat [-a ADDRS] [-b] [-c] [-B] [-d] [-e] [-E] [-f] [-h] [-i] [-I ID]
- [-j] [-l] [-m] [-p PATH] [-R] [-r] [-s] [-t N] [-u] [-v] [-V]
- [-w W] [-x] [-z] VOL-IMAGE
- DESCRIPTION
- This script was written specifically to aid the diagnosis of a corrupted
- filesystem on a DS203 oscilloscope, hardware version 2.72, though it
- may be useful for other small volumes.
- Author:
- Daniel Neville (Blancmange), creamygoat@gmail.com
- Copyright:
- None
- Licence:
- Public domain
- """
- #------------------------------------------------------------------------------
- # Imports
- #------------------------------------------------------------------------------
- import sys
- import traceback
- import os
- import pipes
- import argparse
- from enum import Enum
- #------------------------------------------------------------------------------
- # Constants
- #------------------------------------------------------------------------------
- VERSION = '1.0.0.0'
- DIR_ERRORS_OWN = 0x01
- DIR_ERRORS_CHILDREN = 0x02
- DIR_ERRORS_DESCENDENTS = 0x04
- DIR_SHOW_FLAT = 0x01
- DIR_SHOW_HEX = 0x02
- DIR_SHOW_SPANS = 0x04
- DIR_SHOW_JUNK = 0x08
- DIR_SHOW_CLAIMS = 0x10
- DIR_SHOW_ERRORS = 0x20
- DIR_SHOW_ONLY_ERRORS = 0x40
- #------------------------------------------------------------------------------
- # Exceptions
- #------------------------------------------------------------------------------
- class Error (Exception):
- pass
- class ArgError (Error):
- pass
- class FileError(Error):
- pass
- class DataError(Error):
- pass
- class CmdError(Error):
- pass
- #------------------------------------------------------------------------------
- # Classes
- #------------------------------------------------------------------------------
- class FATRawFile (object):
- def __init__(self):
- self.is_dir = False
- self.name = ""
- self.pathname = ""
- self.attributes = 0x00
- self.size = 0
- self.valid = False
- self.clusters = []
- self.collision_cluster = None
- self.secondary_claims = []
- self.alloc_id = 0
- self.owner_id = None
- self.errors = []
- #------------------------------------------------------------------------------
- class FATFile (FATRawFile):
- def __init__(self):
- super().__init__()
- #------------------------------------------------------------------------------
- class FATDir (FATRawFile):
- #----------------------------------------------------------------------------
- def __init__(self):
- super().__init__()
- self.is_dir = True
- self.parent_cluster = 0
- self.subdirs = []
- self.files = []
- self.volume_name = ""
- self.junk_entries = []
- self.error_depth_flags = 0
- self.last_alloc_id = 0
- #----------------------------------------------------------------------------
- def find(self, pathname):
- result = None
- S = "".join((ch for ch in pathname))
- if S == "":
- result = self
- else:
- # First, set the current directory being searched.
- cdir = self
- # Note that the path fragments may include an empty string
- # at the end, indicating the use of an optional slash, which
- # is sometimes used to specifically indicate a directory in
- # some contexts.
- P = S.split("/")
- while len(P) > 1:
- # The remaining path definitely indicates that
- # a subdirectory must be searched.
- found = False
- for f in cdir.subdirs:
- if f.name == P[0]:
- found = True
- P = P[1:]
- if P[0] == "":
- # A trailing slash was used.
- # f is the directory successfully found
- # cdir is its container
- result = f
- P = []
- cdir = f
- break
- if not found:
- cdir = None
- break
- if result is None and cdir is not None:
- # The scope of the search is now in a single directory.
- # There is no trailing slash in the search string, so the
- # user could be trying to refer to a directory or a file.
- # First, look at the files.
- for f in cdir.files:
- if f.name == P[0]:
- result = f
- break
- if result is None:
- # Now search for subdirectories, perhaps one which
- # (illegally) shares a name with a file.
- for f in cdir.subdirs:
- if f.name == P[0]:
- result = f
- break
- return result
- #----------------------------------------------------------------------------
- #------------------------------------------------------------------------------
- # Span functions
- #------------------------------------------------------------------------------
- def merge_span(spans, new_span):
- low_spans = []
- high_spans = []
- merged_span = new_span
- for span in spans:
- if span[1] < new_span[0]:
- low_spans.append(span)
- elif span[0] > new_span[1]:
- high_spans.append(span)
- else:
- merged_span = (
- min(span[0], merged_span[0]),
- max(span[1], merged_span[1]),
- )
- return tuple(low_spans) + tuple((merged_span,)) + tuple(high_spans)
- #------------------------------------------------------------------------------
- # Helper functions
- #------------------------------------------------------------------------------
- def div_ru(dividend, divisor):
- return -((-dividend) // divisor)
- #------------------------------------------------------------------------------
- def le_uint(le_bytes, offset=None, length=None):
- ix = 0 if offset is None else offset
- rem_length = len(le_bytes) - ix if length is None else length
- result = 0
- shift = 0
- if rem_length > 256:
- raise Error("Cannot handle integers longer than 256 bytes!")
- while rem_length > 0:
- result += le_bytes[ix] << shift
- ix += 1
- shift += 8
- rem_length -= 1
- return result
- #------------------------------------------------------------------------------
- # FATVolumeMetrics
- #------------------------------------------------------------------------------
- class FATVolumeMetrics (object):
- #----------------------------------------------------------------------------
- def __init__(self, boot_sector):
- bs = boot_sector
- self.bs_sig = bs[0x1FE: 0x200]
- self.has_kump = bs[0x000] == 0xE9 or (bs[0] == 0xEB and bs[2] == 0x90)
- self.oem_name = bs[0x003 : 0x00B].decode("iso8859_1").rstrip()
- self.num_reserved_sectors = le_uint(bs, 0x00E, 2)
- ns_16 = le_uint(bs, 0x013, 2)
- self.media_descriptor = bs[0x015]
- self.num_fats = bs[0x010]
- self.num_root_dir_entries = le_uint(bs, 0x011, 2)
- self.sector_size = le_uint(bs, 0x00B, 2)
- self.sectors_per_fat = le_uint(bs, 0x016, 2)
- self.sectors_per_cluster = bs[0x00D]
- self.sectors_per_track = le_uint(bs, 0x018, 2)
- self.num_heads = le_uint(bs, 0x01A, 2)
- self.num_hidden_sectors = le_uint(bs, 0x01C, 4)
- ns_32 = le_uint(bs, 0x020, 4)
- self.drive_number = bs[0x024]
- self.flags = bs[0x025]
- self.ext_boot_sig = bs[0x026]
- self.serial_number = le_uint(bs, 0x027, 4)
- self.partition_label = bs[0x02B : 0x036].decode("iso8859_1").rstrip()
- self.fs_name = bs[0x036 : 0x03C].decode("iso8859_1").rstrip()
- spc = self.sectors_per_cluster
- if ns_16 != 0:
- if ns_32 != 0:
- self.num_sectors = min(ns_16, ns_32)
- else:
- self.num_sectors = ns_16
- else:
- self.num_sectors = ns_32
- self.root_dir_size_in_sectors = div_ru(
- self.num_root_dir_entries * 32, self.sector_size
- )
- fats_so = self.num_reserved_sectors
- root_so = fats_so + self.num_fats * self.sectors_per_fat
- data_so = root_so + self.root_dir_size_in_sectors
- self.num_clusters = (self.num_sectors - data_so) // spc
- data_end_so = data_so + self.num_clusters * spc
- self.fat_offset = fats_so * self.sector_size
- self.fat_size = self.sectors_per_fat * self.sector_size
- self.root_dir_offset = root_so * self.sector_size
- self.da_offset = data_so * self.sector_size
- self.end_offset = self.num_sectors * self.sector_size
- self.cluster_size = spc * self.sector_size
- self.num_unreachable_sectors = self.num_sectors - data_end_so
- self.min_c = 0x002
- self.max_c = self.min_c + self.num_clusters - 1
- if self.fs_name == "FAT12":
- self.c_mask = 0xFFF
- self.cm_base = 0xFF0
- self.cme_nibble_values_set = 0xFF01
- self.cfw = 3
- elif self.fs_name == "FAT16":
- self.c_mask = 0xFFFF
- self.cm_base = 0xFFF0
- self.cme_nibble_values_set = 0xFF00
- self.cfw = 4
- else:
- self.c_mask = 0x0FFFFFFF
- self.cm_base = 0x0FFFFFF0
- self.cme_nibble_values_set = 0xFF00
- self.cfw = 8
- self.afw = max(6, 2 * div_ru(len("{:x}".format(self.end_offset - 1)), 2))
- valid_fs_names = ("FAT12", "FAT16", "FAT32")
- is_vaguely_valid = (
- self.fs_name in valid_fs_names
- and self.ext_boot_sig == 0x29
- and self.num_fats > 0
- and self.num_root_dir_entries > 0
- and self.da_offset + self.cluster_size <= self.end_offset
- )
- self.hopeless = not is_vaguely_valid
- #----------------------------------------------------------------------------
- def dc_vol_offset(self, cluster):
- if self.min_c <= cluster <= self.max_c:
- return self.da_offset + (cluster - self.min_c) * self.cluster_size
- else:
- raise Error("Data cluster index out of bounds for FAT.")
- #----------------------------------------------------------------------------
- def cluster_from_offset(self, offset_in_vol):
- if self.da_offset <= offset_in_vol < self.end_offset:
- return (
- self.min_c
- + (offset_in_vol - self.da_offset) // self.cluster_size
- )
- else:
- raise Error("Offset in volume out of bounds of data area.")
- #----------------------------------------------------------------------------
- def fmt_c(self, cluster):
- return "{:0{}X}".format(cluster, self.cfw)
- #----------------------------------------------------------------------------
- def fmt_a(self, cluster):
- return "{:0{}X}".format(cluster, self.afw)
- #----------------------------------------------------------------------------
- def is_valid_data_cref(self, cref):
- # No masking is performed. The extra bits in FAT32 which might appear
- # in a FAT entry must be masked out already.
- return self.min_c <= cref <= self.max_c
- #----------------------------------------------------------------------------
- def is_last_in_chain_marker(self, cref):
- # As a last-in-chain marker appears within a FAT entry and has little
- # meaning outside of a FAT, the masking is applied by this function.
- c = cref & self.c_mask
- return (c >= self.cm_base
- and self.cme_nibble_values_set & (1 << (c & 15)) != 0)
- #----------------------------------------------------------------------------
- def cluster_kind(self, cluster_index, cluster_value):
- # So far bits 28-31 in FAT32 cluster values are ignored.
- ch = "?"
- cv = cluster_value & self.c_mask
- if cluster_index == 0:
- # The first entry in the FAT holds the media descriptor
- # in the lower eight bits of the cluster value, corresponding
- # to the very first byte of the FAT.
- #
- # The rest of the bits of the cluster value should be all ones.
- # (Equivalantly, the cluster value should be greater than or
- # equal to cm_base, the Cluster Marker Base value.
- if cv >= self.cm_base and cv & 0xFF == self.media_descriptor:
- ch = "S"
- elif cluster_index == 1:
- # The second entry in the FAT holds an end marker. The entry
- # refers to no valid data region in contrast to normal cluster
- # entries in the FAT that are end markers to indicate that their
- # corresponding region within the data area is the last cluster
- # in a chain. This end marker may as well be regarded a part of
- # the signature.
- if self.is_last_in_chain_marker(cv):
- ch = "S"
- elif self.min_c <= cluster_index <= self.max_c:
- # The cluster index corresponds to a fat entry which corresponds
- # to a Data Cluster, a region within the data area.
- #
- # Now all tests are performed on the cluster value. For non-terminal
- # data clusters, the cluster value is a reference to another cluster's
- # entry in the FAT. Otherwise the value directly refers to the state
- # of the corresponding data cluster.
- if cv == 0:
- # Free cluster
- ch = "."
- elif cv < self.min_c:
- # Reserved value
- ch = "R"
- elif cv < self.cm_base:
- # Non-terminal entry in a luster chain
- if cv <= self.max_c:
- # Normal cluster in chain
- ch = "d"
- else:
- # Link to data beyond the end of the data area
- # nevertheless addressable by the FAT.
- ch = "f"
- else:
- # High values with special meaning.
- cv4 = cv & 0xF
- if self.cme_nibble_values_set & (1 << cv4):
- # Last cluster in chain
- ch = "e"
- else:
- if cv4 == 0:
- # Reserved value, except in FAT12, where it may be used
- # as an end marker. (The above test catches that.)
- ch = "R"
- elif cv4 < 7:
- # Reserved value
- ch = "R"
- elif cv4 == 7:
- # The data cluster is marked as bad.
- ch = "B"
- return ch
- #----------------------------------------------------------------------------
- #------------------------------------------------------------------------------
- def get_fat(m, fat_img, full_fat=False):
- result = []
- if full_fat:
- if m.fs_name == "FAT12":
- fat_len = (m.fat_size * 2) // 3
- elif m.fs_name == "FAT16":
- fat_len = (m.fat_size) // 2
- else:
- fat_len = (m.fat_size) // 4
- else:
- fat_len = m.max_c + 1
- if m.fs_name == "FAT12":
- num_whole_triple_bytes = fat_len // 2
- for i in range(num_whole_triple_bytes):
- x = le_uint(fat_img, 3 * i, 3)
- result.append(x & 0x000FFF)
- result.append(x >> 12)
- if fat_len & 1:
- x = le_uint(fat_img, 3 * num_whole_triple_bytes, 2) & 0x0FFF
- result.append(x)
- elif m.fs_name == "FAT16":
- for i in range(fat_len):
- result.append(le_uint(fat_img, 2 * i, 2))
- else:
- raise Error('Unsupported Filesystem type, "{}".'.format(m.fs_name))
- return result
- #------------------------------------------------------------------------------
- def walk_fat(m, fat, volume_file, part_offset=0):
- #----------------------------------------------------------------------------
- def walk_recursive(
- fdir,
- dir_img, volume_file, part_offset,
- m, fat, id_map, id_list,
- alloc_id):
- valid_dos_chars = (
- "0123456789"
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- " !#$%&'()-@^_`{}~"
- )
- fdir.alloc_id = alloc_id
- fdir.last_alloc_id = alloc_id
- next_id = alloc_id + 1
- num_entries = len(dir_img) // 32
- is_root = len(fdir.clusters) == 0
- volume_name = None
- for i in range(num_entries):
- entry = dir_img[32 * i : 32 * (i + 1)]
- name_field = entry[:0x0B]
- status = name_field[0]
- attributes = entry[0x0B] # 00ADVSHR
- start_c = le_uint(entry, 0x1A, 2)
- file_size = le_uint(entry, 0x1C, 4)
- if status in [0x05, 0xE5]:
- status ^= 0xE0
- if status == 0:
- # A zero is used to terminator a directory.
- break
- elif status == 0x2E:
- # Link to current or parent directory (or root)
- name = ".." if entry[1] == 0x2E else "."
- if is_root:
- fdir.errors.append(
- 'Spurious "{}" (Cx{}) found in root.'.format(
- name, m.fmt_c(start_c)))
- else:
- if entry[1] == 0x2E:
- # ".." (link to parent)
- fdir.parent_cluster = start_c
- # Check parentage later.
- else:
- # "." (link to self)
- if start_c != fdir.clusters[0]:
- fdir.errors.append('Bad ".": (Cx{})'.format(m.fmt_c(start_c)))
- else:
- # The name field is now worth processing.
- S = []
- for x in name_field:
- ch = chr(x) if chr(x) in valid_dos_chars else "?"
- S.append(ch)
- S = "".join(S)
- base_name = S[:8].rstrip()
- ext_name = S[8 : 11].rstrip()
- name = base_name if ext_name == "" else base_name + "." + ext_name
- if attributes == 0x0F != 0:
- # VFAT long file name fragment
- # The true attributes are stored in the preceding
- # DOS filename entry for the same file.
- fdir.junk_entries.append(name)
- elif attributes & 0x08 != 0:
- # Volume Name
- vn = S.rstrip()
- if is_root:
- if volume_name is None:
- volume_name = vn
- fdir.volume_name = volume_name
- else:
- fdir.errors.append('Redundant volume name: "{}"'.format(vn))
- else:
- fdir.errors.append('Spurious volume name: "{}"'.format(vn))
- elif status == 0x05:
- # Deleted file
- # In really old versions of DOS, deleted files still reserved
- # space on volume until a garbage collector program was issued.
- # In later versions, deleted files had their cluster chains in
- # the FAT zeroed, which made recovering a deleted file's data
- # from the data clusters a hit-and-miss affair.
- fdir.junk_entries.append(name)
- elif attributes & 0x10 != 0 and start_c == 0:
- # A subdirectory with no starting cluster is invalid since it
- # cannot store entries even for "." and "..".
- fdir.junk_entries.append(name)
- else:
- if attributes & 0x10 != 0:
- f = FATDir()
- is_dir = True
- f.name = name
- f.pathname = fdir.pathname + f.name + "/"
- else:
- f = FATFile()
- f.size = file_size
- is_dir = False
- f.name = name
- f.pathname = fdir.pathname + f.name
- chain_valid = True
- f.attributes = attributes
- f.owner_id = fdir.alloc_id
- f.alloc_id = next_id
- fdir.last_alloc_id = f.alloc_id
- id_list.append(f)
- next_id += 1
- # In a directory entry, a start cluster references of zero
- # implies a chain length of zero. A zero (illegal) and some
- # values for next-cluster references appearing within a
- # cluster chain acts as a terminator, causing the cluster
- # in which that zero appears to be excluded. In contrast,
- # a last-in-chain marker implies that the cluster is included
- # in the chain but no more clusters follow.
- if is_dir:
- if file_size != 0:
- f.errors.append(
- ("File size for directory is {} "
- + "but should be zero.").format(FileSize))
- if not m.is_valid_data_cref(start_c):
- f.errors.append(
- "Bad subdirectory start cluster: Cx{}".format(
- m.fmt_c(start_c)))
- chain_valid = False
- else:
- if file_size != 0:
- if not m.is_valid_data_cref(start_c):
- f.errors.append(
- "Bad file start cluster: Cx{}".format(m.fmt_c(start_c)))
- chain_valid = False
- else:
- if start_c != 0:
- f.errors.append(
- "Empty file has non-zero start cluster: Cx{}.".format(
- m.fmt_c(start_c)))
- chain_valid = False
- rem_file_size = file_size
- if chain_valid and (is_dir or rem_file_size > 0):
- cluster = 0
- next_c = start_c
- while True:
- if m.is_valid_data_cref(next_c):
- if not is_dir:
- if rem_file_size > 0:
- rem_file_size = max(0, rem_file_size - m.cluster_size)
- else:
- chain_valid = False
- f.errors.append(
- "Last file cluster at Cx{} links to Cx{}.".format(
- m.fmt_c(cluster), m.fmt_c(next_c)))
- break
- next_c_id = id_map[next_c]
- if next_c_id == 0:
- cluster = next_c
- next_c = fat[cluster] & m.c_mask
- id_map[cluster] = f.alloc_id
- f.clusters.append(cluster)
- else:
- chain_valid = False
- f.collision_cluster = next_c
- if cluster > 0:
- src_str = "Cluster Cx{}".format(m.fmt_c(cluster))
- else:
- src_str = "Start"
- owner_id = next_c_id
- looped = owner_id == f.alloc_id
- if not looped:
- # Each secondary claim record consists of:
- # * The file offset (in clusters) where the claim occurs;
- # * The allocation ID of the claimant and
- # * The offset (in clusters) within the claimant.
- fco = len(f.clusters)
- owner_f = id_list[owner_id]
- owner_fco = owner_f.clusters.index(f.collision_cluster)
- owner_f.secondary_claims.append(
- (owner_fco, f.alloc_id, fco)
- )
- if looped:
- em = "Loop: {} -> Cx{}".format(src_str, m.fmt_c(next_c))
- else:
- em = "Collision: {} (ID {}) -> Cx{} (ID {})".format(
- src_str, f.alloc_id, m.fmt_c(next_c), next_c_id)
- f.errors.append(em)
- break
- elif m.is_last_in_chain_marker(next_c):
- break
- else:
- chain_valid = False
- f.errors.append(
- "Bad cluster link: Cx{} -> Cx{}".format(
- m.fmt_c(cluster), m.fmt_c(next_c)))
- break
- if is_dir:
- # Even though the directory entry should always record a
- # file size of zero for a subdirectory, the file size of
- # a subdirectory is determined by the number of clusters
- # in its chain.
- f.size = len(f.clusters) * m.cluster_size
- if chain_valid:
- if len(f.clusters) < 1:
- f.errors.append("Subdirectory has no (valid) clusters.")
- chain_valid = False
- f.valid = chain_valid
- subdir_img = bytearray()
- for c in f.clusters:
- offset_to_dc = m.dc_vol_offset(c)
- volume_file.seek(part_offset + offset_to_dc)
- dir_frag = volume_file.read(m.cluster_size)
- subdir_img += dir_frag
- walk_recursive(
- f,
- subdir_img, volume_file, part_offset,
- m, fat, id_map, id_list,
- f.alloc_id
- )
- expected_parent = (fdir.clusters[:1] + [0])[0]
- if f.parent_cluster != expected_parent:
- f.errors.append(
- "Bad parent reference: Cx{}".format(
- m.fmt_c(f.parent_cluster)))
- f.parent_cluster = expected_parent
- fdir.last_alloc_id = f.last_alloc_id
- next_id = fdir.last_alloc_id + 1
- fdir.subdirs.append(f)
- if f.error_depth_flags & DIR_ERRORS_OWN != 0:
- fdir.error_depth_flags |= DIR_ERRORS_CHILDREN
- if f.error_depth_flags != 0:
- fdir.error_depth_flags |= DIR_ERRORS_DESCENDENTS
- else:
- if chain_valid:
- expected_length_in_clusters = div_ru(file_size, m.cluster_size)
- diff = len(f.clusters) - expected_length_in_clusters
- if diff != 0:
- chain_valid = False
- if diff < 0:
- f.errors.append(
- "Truncated: {} clusters short, {} bytes lost.".format(
- -diff, rem_file_size))
- else:
- # This condition cannot happen if the check
- # with the remaining file size is working.
- f.errors.append(
- "Chain is too long by {} clusters.".format(diff))
- if not chain_valid:
- f.errors.append(
- ("{} clusters collected from " +
- "invalid chain.").format(len(f.clusters)))
- f.valid = chain_valid
- fdir.files.append(f)
- if len(f.errors) > 0:
- fdir.error_depth_flags |= (
- DIR_ERRORS_CHILDREN | DIR_ERRORS_DESCENDENTS)
- if len(fdir.errors) > 0:
- fdir.error_depth_flags |= DIR_ERRORS_OWN
- return fdir
- #----------------------------------------------------------------------------
- volume_file.seek(part_offset + m.root_dir_offset)
- root_dir_img = volume_file.read(m.num_root_dir_entries * 32)
- root_dir = FATDir()
- id_map = [0] * len(fat)
- id_list = [root_dir]
- walk_recursive(
- root_dir,
- root_dir_img, volume_file, part_offset,
- m, fat, id_map, id_list,
- 0
- )
- return (root_dir, id_map, id_list)
- #------------------------------------------------------------------------------
- def all_dirs(fdir):
- yield fdir
- for subdir in fdir.subdirs:
- yield from all_dirs(subdir)
- #------------------------------------------------------------------------------
- def all_dirs_and_files(fdir):
- for fdir in all_dirs(fdir):
- for f in [fdir] + fdir.files:
- yield f
- #------------------------------------------------------------------------------
- def secondary_claims_for_file(m, f, id_map, id_list):
- for sc in f.secondary_claims:
- yield sc
- fso, claimant_id, claimant_fco = sc
- claimant_f = id_list[claimant_id]
- for claimant_sc in secondary_claims_for_file(
- m, claimant_f, id_map, id_list):
- r_fco, r_id, r_fco = claimant_sc
- r_f = id_list[r_id]
- x = r_fco + (claimant_fco - r_fco)
- # So far, (fco, r_id, x) would be the composite remote claim
- # for this file. First, we should check the remote file's length.
- # A file does not claim a cluster just because its cluster chain
- # ultimately leads to that cluster. Reading a file always requires
- # knowledge of the file size in bytes.
- rclen = div_ru(r_f.file_size, m.cluster_size)
- if x < rclen:
- yield (fco, r_id, x)
- #------------------------------------------------------------------------------
- def analyse_addr_in_volume(
- m, addr, fat, id_map, id_list):
- da_end = m.da_offset + m.num_clusters * m.cluster_size
- cluster_ix = None
- desc = "(Nowhere)"
- f = None
- c = None
- offset = None
- if not m.da_offset <= addr < da_end:
- # Annoying cases
- if addr < 0:
- # Negative address
- desc = "Before volume"
- offset = addr
- elif addr < m.fat_offset:
- # Boot sector
- offset = addr
- desc = "Boot sector".format(offset)
- elif addr < m.root_dir_offset:
- # FATs
- ix = (addr - m.fat_offset) // m.fat_size
- offset = addr - m.fat_size * ix
- desc = "FAT #{}".format(ix)
- elif addr < m.da_offset:
- a = addr - m.root_dir_offset
- ix = a // 32
- if ix < m.num_root_dir_entries:
- f = id_list[0]
- offset = a & 31
- desc = "Root directory item {}".format(ix)
- else:
- offset = a
- desc = "Root directory"
- else:
- if addr == da_end:
- desc = "End of data area"
- offset == None
- else:
- desc = "Beyond end of data area"
- offset = addr - da_end
- else:
- # The interesting case in which the supplied address is
- # within the data area.
- c = ((addr - m.da_offset) // m.cluster_size) + m.min_c
- c_addr = m.da_offset + (c - m.min_c) * m.cluster_size
- item_index = c
- offset = addr - c_addr
- if c > m.max_c:
- desc = "Unreachable cluster"
- else:
- aid = id_map[c]
- if aid == 0:
- # This cluster is not referenced by the directory tree.
- # Not properly, at least.
- k = m.cluster_kind(c, fat[c])
- if k == ".":
- desc = "Free cluster"
- elif k in "def":
- desc = "Orphan cluster"
- elif k == "B":
- desc = "Bad cluster"
- else:
- desc = "Unknown cluster"
- else:
- # We have a properly owned cluster.
- f = id_list[aid]
- fcn = f.clusters.index(c) # File cluster number
- offset = fcn * m.cluster_size + offset
- desc = f.pathname
- return (c, f, offset, desc)
- #------------------------------------------------------------------------------
- def get_fat_usage(m, fat, id_map=None):
- num_used = 0
- num_free = 0
- num_reserved = 0
- num_orphaned = 0
- num_bad = 0
- num_invalid_ce = 0
- for c in range(m.min_c, m.max_c + 1):
- ch = m.cluster_kind(c, fat[c])
- if ch == ".":
- num_free += 1
- elif ch in "def":
- if ch =="f":
- num_invalid_ce += 1
- if id_map is not None and id_map[c] > 0:
- num_used += 1
- else:
- num_orphaned += 1
- elif ch in ["B"]:
- num_bad += 1
- elif ch == "R":
- num_reserved += 1
- result = {
- "Total": m.num_clusters,
- "Used": num_used,
- "Free": num_free,
- "Reserved": num_reserved,
- "Bad": num_bad,
- "Orphaned": num_orphaned,
- "InvalidCE": num_invalid_ce
- }
- return result
- #------------------------------------------------------------------------------
- # Report generating functions
- #------------------------------------------------------------------------------
- def metrics_report(m):
- yield 'Size: {} sectors ({} bytes)'.format(
- m.num_sectors, m.num_sectors * m.sector_size)
- yield 'OEM: "{}"'.format(m.oem_name)
- yield 'Partition Label: "{}"'.format(m.partition_label)
- yield 'Media Descriptor: 0x{:02X}'.format(m.media_descriptor)
- yield 'EBPB Signature: 0x{:02X}'.format(m.ext_boot_sig)
- yield 'Serial Number: {:04X}-{:04X}'.format(
- m.serial_number >> 16, m.serial_number & 0xFFFF)
- yield 'Filesystem: "{}"'.format(m.fs_name)
- yield 'Sector Size: {} bytes'.format(m.sector_size)
- yield 'Cluster Size: {} sectors'.format(m.sectors_per_cluster)
- yield 'FAT Size: {} bytes'.format(m.sector_size)
- yield 'Number of FATs: {}, each {} sectors long'.format(
- m.num_fats, m.sectors_per_fat)
- yield 'Flags: 0b{0:08b} (0x{0:02X})'.format(m.flags)
- yield 'Data Area: {} clusters ({} bytes)'.format(
- m.num_clusters, m.num_clusters * m.cluster_size)
- yield 'Unreachable Sectors at End of Data Area: {}'.format(
- m.num_unreachable_sectors)
- if m.num_fats > 0:
- yield "FAT #0 (default): 0x{}".format(m.fmt_a(m.fat_offset))
- for i in range(1, m.num_fats):
- yield "Alternate FAT #{}: 0x{}".format(
- i, m.fmt_a(m.fat_offset + i * m.fat_size))
- yield "Valid Cluster Entry Range: Cx{}..Cx{}".format(
- m.fmt_c(m.min_c), m.fmt_c(m.max_c))
- yield "Root Dir: 0x{}".format(m.fmt_a(m.root_dir_offset))
- yield "Data Area: 0x{}".format(m.fmt_a(m.da_offset))
- yield "Data end: 0x{}".format(m.fmt_a(m.end_offset))
- #------------------------------------------------------------------------------
- def spans_report(m, clusters, file_size, collision_cluster):
- def cluster_spans(clusters):
- first_c = None
- last_c = None
- cix = 0
- while cix < num_whole_clusters:
- c = clusters[cix]
- if last_c is None:
- first_c = c
- elif c != last_c + 1:
- yield (first_c, last_c)
- first_c = c
- last_c = c
- cix += 1
- if last_c is not None:
- yield (first_c, last_c)
- num_whole_clusters = min(len(clusters), file_size // m.cluster_size)
- rem_bytes = file_size - num_whole_clusters * m.cluster_size
- cr_fw = 6 + 2 * m.cfw
- file_offset = 0
- for first, last in cluster_spans(clusters[:num_whole_clusters]):
- span_size = (last + 1 - first) * m.cluster_size
- vol_offset = m.dc_vol_offset(first)
- fo_str = m.fmt_a(file_offset)
- cr_str = "Cx" + m.fmt_c(first)
- if first != last:
- cr_str += "..Cx" + m.fmt_c(last)
- vr_str = "{}:{}".format(
- m.fmt_a(vol_offset),
- m.fmt_a(vol_offset + span_size))
- yield "{} {:{}} {}".format(fo_str, cr_str, cr_fw, vr_str)
- file_offset += span_size
- for c in clusters[num_whole_clusters:]:
- span_size = rem_bytes
- vol_offset = m.dc_vol_offset(c)
- fo_str = m.fmt_a(file_offset)
- cr_str = "Cx{} (part)".format(m.fmt_c(c))
- vr_str = "{}:{}".format(
- m.fmt_a(vol_offset),
- m.fmt_a(vol_offset + span_size))
- yield "{} {:{}} {}".format(fo_str, cr_str, cr_fw, vr_str)
- file_offset += span_size
- fo_str = m.fmt_a(file_offset)
- if collision_cluster is not None:
- fault_str = " Collision at Cx{}".format(m.fmt_c(collision_cluster))
- elif len(clusters) * m.cluster_size < file_size:
- fault_str = " Truncated"
- else:
- fault_str = ""
- yield "{}{}".format(fo_str, fault_str)
- #------------------------------------------------------------------------------
- def secondary_claims_report(m, f, id_map, id_list, disp_hex=False):
- def fmt_o(offset):
- return ("0x{:X}" if disp_hex else "{}").format(offset)
- for sc in secondary_claims_for_file(m, f, id_map, id_list):
- fco, claimant_id, claimant_fco = sc
- claimant_f = id_list[claimant_id]
- collision_cluster = f.clusters[fco]
- yield (
- "Cx{} {}: Byte {} is byte {} in {}".format(
- m.fmt_c(collision_cluster),
- m.fmt_a(m.dc_vol_offset(collision_cluster)),
- fmt_o(m.cluster_size * fco),
- fmt_o(m.cluster_size * claimant_fco),
- claimant_f.pathname
- )
- )
- #------------------------------------------------------------------------------
- def dir_report(
- m, fdir, ffile, opts,
- id_map, id_list,
- level=0, max_level=None,
- indent_str=" "):
- def attrs_to_str(a):
- # 00ADVSHR
- def flag(attrs, bit, ch):
- return ch if attrs & (1 << bit) != 0 else "-"
- return "".join([
- flag(a, 5, "A"),
- flag(a, 3, "V"),
- flag(a, 2, "S"),
- flag(a, 1, "H"),
- flag(a, 0, "R"),
- ])
- def dec_fmt_fn(x):
- return str(x)
- def hex_fmt_fn(x):
- return "0x{:X}".format(x)
- err_prefix = "(!) "
- disp_hex = (opts & DIR_SHOW_HEX) != 0
- show_spans = (opts & DIR_SHOW_SPANS) != 0
- show_claims = (opts & DIR_SHOW_CLAIMS) != 0
- only_errors = (opts & DIR_SHOW_ONLY_ERRORS) != 0
- flat = (opts & DIR_SHOW_FLAT != 0)
- show_junk = (opts & DIR_SHOW_JUNK != 0) and not only_errors
- show_errors = (opts & DIR_SHOW_ERRORS != 0) or only_errors
- if flat:
- i_s = ""
- else:
- i_s = indent_str * level
- # Byte size and offset format function
- bso_fmt_fn = hex_fmt_fn if disp_hex else dec_fmt_fn
- has_own_error = len(fdir.errors) > 0
- has_child_error = (fdir.error_depth_flags & DIR_ERRORS_CHILDREN) != 0
- has_descendent_error = (fdir.error_depth_flags & DIR_ERRORS_DESCENDENTS) != 0
- has_any_error = has_own_error or has_descendent_error
- is_root = fdir.owner_id is None
- do_report_self = (
- (is_root and has_any_error)
- or (flat and has_child_error)
- or (not flat and has_descendent_error))
- if flat:
- # In flat, non-nested mode, a directory heading is
- # required even for the top level.
- if do_report_self or not only_errors:
- if is_root:
- if fdir.volume_name is not None and fdir.volume_name != "":
- pathname_str = "Volume {}".format(fdir.volume_name)
- elif m.partition_label != "":
- pathname_str = "Partition {}".format(m.partition_label)
- else:
- pathname_str = "/"
- else:
- pathname_str = fdir.pathname
- yield(pathname_str + ":")
- if is_root and show_errors and ffile is None:
- for e in fdir.errors:
- yield "{}{}{}".format(i_s, err_prefix, str(e))
- # List the subdirectories.
- for d in fdir.subdirs:
- if ((ffile is None or d is ffile)
- and (len(d.errors) > 0 or not only_errors)):
- start_c = (d.clusters[:1] + [0])[0]
- yield "{}{:13}{:>12} {} at Cx{} {:7} ID: {}".format(
- i_s,
- d.name + "/",
- "",
- attrs_to_str(d.attributes),
- m.fmt_c(start_c),
- "",
- d.alloc_id,
- )
- if show_spans:
- dir_file_size = max(1, len(d.clusters)) * m.cluster_size
- for line in spans_report(
- m, d.clusters, dir_file_size, d.collision_cluster):
- yield "{}{}{}".format(i_s, indent_str, line)
- if show_claims:
- for line in secondary_claims_report(
- m, d, id_map, id_list, disp_hex):
- yield "{}{}{}".format(i_s, indent_str, line)
- if show_errors:
- for e in d.errors:
- yield "{}{}{}{}".format(i_s, indent_str, err_prefix, str(e))
- # Only recurse through the displayed list of subdirectories
- # if nested mode is selected. (Non-nested recursion is to
- # occur at the bottom instead.)
- if ffile is None and not flat:
- if max_level is None or max_level < 0 or level < max_level:
- yield from dir_report(
- m, d, None, opts, id_map, id_list, level + 1, max_level, indent_str
- )
- for f in fdir.files:
- size_str = "({})".format(bso_fmt_fn(f.size))
- if len(f.clusters) > 0:
- start_c = f.clusters[0]
- start_c_str = "Cx{}".format(m.fmt_c(start_c))
- if start_c > m.max_c:
- start_c_str = start_c_str + "!"
- c_count = div_ru(f.size, m.cluster_size)
- if len(f.clusters) == c_count:
- c_count_str = "{}".format(c_count)
- else:
- c_count_str = "{}/{}!".format(len(f.clusters), c_count)
- c_start_count_str = "at {} ({})".format(start_c_str, c_count_str)
- else:
- c_start_count_str = ""
- if ((ffile is None or f is ffile)
- and (len(f.errors) > 0 or not only_errors)):
- yield "{}{:12} {:>12} {} {:{}} ID: {}".format(
- i_s,
- f.name,
- size_str,
- attrs_to_str(f.attributes),
- c_start_count_str, 13 + m.cfw,
- f.alloc_id,
- )
- if show_spans:
- for line in spans_report(
- m, f.clusters, f.size, f.collision_cluster):
- yield "{}{}{}".format(i_s, indent_str, line)
- if show_claims:
- for line in secondary_claims_report(
- m, f, id_map, id_list, disp_hex):
- yield "{}{}{}".format(i_s, indent_str, line)
- if show_errors:
- for e in f.errors:
- yield "{}{}{}{}".format(i_s, indent_str, err_prefix, str(e))
- if ffile is None:
- # No specific file is requested.
- if show_junk:
- # Display the junk entries, including VFAT file names and deleted files.
- for j in fdir.junk_entries:
- yield "{}{:12} <JUNK>".format(i_s, j)
- # The current directory is done.
- # Recursion in flat (non-nested) mode can thus begin here.
- if flat:
- if max_level is None or max_level < 0 or level < max_level:
- for d in fdir.subdirs:
- if ((d.error_depth_flags & DIR_ERRORS_DESCENDENTS != 0)
- or not only_errors):
- yield("")
- yield from dir_report(
- m, d, None, opts,
- id_map, id_list,
- level + 1, max_level, indent_str)
- #------------------------------------------------------------------------------
- def elided_text_lines(
- addr_line_gen,
- elide_repeats=True,
- sep=""):
- prev_line = None
- eliding = False
- for (addr, line) in addr_line_gen():
- if line is not None:
- if (
- elide_repeats and prev_line is not None
- and line == prev_line[:len(line)]
- ):
- if not eliding:
- eliding = True
- yield "*"
- else:
- eliding = False
- prev_line = line
- yield "{}{}{}".format(addr, sep, line)
- else:
- yield addr
- #------------------------------------------------------------------------------
- def brief_map_report(
- m, fat,
- data_only,
- elide_repeats=True, columns=64):
- def al_gen():
- for line_start_ix in range(0, len(fat), columns):
- fat_line = fat[line_start_ix : line_start_ix + columns]
- line = ["?"] * len(fat_line)
- for i, x in enumerate(fat_line):
- c = line_start_ix + i
- ch = m.cluster_kind(c, x)
- ch = "." if data_only and ch not in "def" else ch
- line[i] = ch
- S = "".join(line)
- yield (m.fmt_c(line_start_ix), S)
- yield (m.fmt_c(line_start_ix), None)
- yield from elided_text_lines(al_gen, elide_repeats, ": ")
- #------------------------------------------------------------------------------
- def fancy_brief_map_report(
- m, fat, root_dir,
- data_only, sel_ids,
- id_map,
- elide_repeats=True, columns=64,
- be_fancy=True):
- DATA_BEGIN = 0x01
- LOOP_BEGIN = 0x02
- LOOP_END = 0x04
- COLLISION = 0x08
- ORPHAN = 0x10
- def al_gen():
- aug_map = [0] * len(fat)
- for c in range(2, len(fat)):
- if id_map[c] == 0:
- aug_map[c] |= ORPHAN
- for f in all_dirs_and_files(root_dir):
- for sc in f.secondary_claims:
- aug_map[f.clusters[sc[0]]] |= COLLISION
- if len(f.clusters) > 0:
- aug_map[f.clusters[0]] |= DATA_BEGIN
- if f.collision_cluster in f.clusters:
- aug_map[f.collision_cluster] |= LOOP_BEGIN
- aug_map[f.clusters[-1]] |= LOOP_END
- adj_map_start = {
- "d": "D",
- "e": "E",
- "f": "F",
- }
- adj_map_orphan = {
- "d": "x",
- "e": "y",
- "f": "z",
- }
- for line_start_ix in range(0, len(fat), columns):
- fat_line = fat[line_start_ix : line_start_ix + columns]
- line = ["?"] * len(fat_line)
- for i, x in enumerate(fat_line):
- aid = id_map[i]
- if sel_ids is None or aid in sel_ids:
- c = line_start_ix + i
- ch = m.cluster_kind(c, x)
- ch = "." if data_only and ch not in "def" else ch
- if be_fancy:
- a = aug_map[line_start_ix + i]
- if a & ORPHAN and ch in adj_map_orphan:
- ch = adj_map_orphan[ch]
- if a & DATA_BEGIN and ch in adj_map_start:
- ch = adj_map_start[ch]
- if a & LOOP_BEGIN:
- ch = "[" if a & LOOP_END == 0 else "@"
- elif a & LOOP_END:
- ch = "]"
- if a & COLLISION:
- ch = "*"
- else:
- ch = "."
- line[i] = ch
- S = "".join(line)
- yield (m.fmt_c(line_start_ix), S)
- yield (m.fmt_c(line_start_ix), None)
- yield from elided_text_lines(al_gen, elide_repeats, ": ")
- #------------------------------------------------------------------------------
- def cluster_report(m, fat, data_only, elide_repeats=True, columns=8):
- def al_gen():
- masked_str = "." * m.cfw
- for line_start_ix in range(0, len(fat), columns):
- L = []
- for i, x in enumerate(fat[line_start_ix : line_start_ix + columns]):
- c = line_start_ix + i
- if data_only and m.cluster_kind(c, x) not in "def":
- L.append(masked_str)
- else:
- L.append(m.fmt_c(x))
- S = " ".join(L)
- yield (m.fmt_c(line_start_ix), S)
- yield (m.fmt_c(line_start_ix), None)
- yield from elided_text_lines(al_gen, elide_repeats, ": ")
- #------------------------------------------------------------------------------
- def selective_cluster_report(
- m, fat,
- data_only, sel_ids,
- id_map,
- elide_repeats=True, columns=8):
- def al_gen():
- masked_str = "." * m.cfw
- for line_start_ix in range(0, len(fat), columns):
- L = []
- for i, x in enumerate(fat[line_start_ix : line_start_ix + columns]):
- c = line_start_ix + i
- if sel_ids is not None and id_map[c] not in sel_ids:
- S = masked_str
- else:
- if data_only and m.cluster_kind(c, x) not in "def":
- S = masked_str
- else:
- S = m.fmt_c(x)
- L.append(S)
- S = " ".join(L)
- yield (m.fmt_c(line_start_ix), S)
- yield (m.fmt_c(line_start_ix), None)
- yield from elided_text_lines(al_gen, elide_repeats, ": ")
- #------------------------------------------------------------------------------
- def fat_usage_report(m, usage_stats):
- total = usage_stats["Total"]
- def output_line(key_name, count):
- f = float(count) / total
- return "{:9} {:5} ({:3.1f}%)".format(key_name + ":", count, 100.0 * f)
- yield "FAT allocation in clusters of {} bytes:".format(m.cluster_size)
- yield "{:9} {:5}".format("Total:", total)
- for key in ("Used", "Free", "Orphaned", "Bad"):
- yield output_line(key, usage_stats[key])
- for (key, key_output) in (
- ("Reserved", "Reserved"),
- ("InvalidCE", "Cluster entries with invalid references"),
- ):
- if usage_stats[key] > 0:
- yield output_line(key_ouput, usage_stats[key])
- #------------------------------------------------------------------------------
- def index_report(m, root_dir, id_list):
- fmt_w = len(str(root_dir.last_alloc_id))
- for aid in range(1, len(id_list)):
- f = id_list[aid]
- if len(f.clusters) == 0:
- cs_str = " " * (m.cfw + 2)
- else:
- cs_str = "Cx" + m.fmt_c(f.clusters[0])
- yield "{:>{}} {} {}".format(aid, fmt_w, cs_str, f.pathname)
- #------------------------------------------------------------------------------
- def addr_report(m, addr, fat, id_map, id_list, disp_hex=False):
- c, f, offset, desc = analyse_addr_in_volume(
- m, addr, fat, id_map, id_list
- )
- def fmt_o(offset):
- return ("0x{:X}" if disp_hex else "{}").format(offset)
- astr = "0x{}".format(m.fmt_a(addr))
- if c is not None:
- c_addr = m.da_offset + m.cluster_size * (c - m.min_c)
- relstr = "at" if c_addr == addr else "in"
- cstr = "({} Cx{})".format(relstr, m.fmt_c(c))
- else:
- cstr = ""
- if offset is not None:
- ostr = ", byte {}".format(fmt_o(offset))
- else:
- ostr = ""
- if c is None:
- yield "{}: {}{}".format(astr, desc, ostr)
- else:
- if f is None:
- yield "{} {} {}{}".format(astr, cstr, desc, ostr)
- else:
- if offset >= f.size:
- ostr += (" (overshot)")
- yield "{} {} {}{}".format(astr, cstr, desc, ostr)
- for sc in secondary_claims_for_file(m, f, id_map, id_list):
- fco, rid, rfco = sc
- collision_cluster = f.clusters[fco]
- if collision_cluster == c:
- rf = id_list[rid]
- x = offset - fco * m.cluster_size
- rfo = rfco * m.cluster_size + x
- rfostr = ", byte {}".format(fmt_o(rfo))
- if rfo >= rf.size:
- rfostr += (" (overshot)")
- yield "(!) Same address as {}{}".format(rf.pathname, rfostr)
- #------------------------------------------------------------------------------
- # Main
- #------------------------------------------------------------------------------
- def main():
- def printlines(seq):
- for line in seq:
- print(line)
- def nice_num_columns(console_width, label_w, column_w):
- var_w = console_width - label_w
- max_cols = var_w // column_w
- group_size = 1
- while 2 * group_size <= max_cols and 2 * group_size <= 16:
- group_size *= 2
- num_groups = max(1, max_cols // group_size)
- return num_groups * group_size
- def ca_addr_strs_from_csv(csv_addrs):
- result = []
- addr_strs = csv_addrs.split(",")
- for addr_str in addr_strs:
- ass = addr_str.strip().upper()
- prefix = ""
- if ass[:2] == "CX" and ass[2 : 3] != " ":
- ass = "0x" + ass[2:]
- variant = "C"
- int_base = 16
- min_a = 2
- elif ass[:1] == "C" and ass[1 : 2] != " ":
- ass = ass[1:]
- variant = "C"
- int_base = 10
- min_a = 2
- else:
- variant = "A"
- int_base = None
- min_a = 0
- try:
- if int_base is not None:
- a = int(ass, int_base)
- else:
- if ass[:2] in ["0x", "0X"]:
- a = int(ass, 16)
- elif ass[:2] in ["0o", "0o"]:
- a = int(ass, 8)
- elif ass[:2] in ["0b", "0b"]:
- a = int(ass, 2)
- else:
- a = int(ass)
- if a < min_a:
- if variant == "C":
- raise argparse.ArgumentTypeError(
- 'A cluster index cannot be less than {}.'.format(min_a))
- else:
- raise argparse.ArgumentTypeError(
- 'An address cannot be negative.')
- result.append(variant + str(a))
- except ValueError as E:
- raise argparse.ArgumentTypeError(
- '"{}" is not a valid address.'.format(addr_str))
- return result
- def get_arguments():
- cmd = os.path.basename(sys.argv[0])
- parser = argparse.ArgumentParser(
- prog=cmd,
- add_help=False,
- description="Examines a FAT12 or FAT16 partition."
- )
- parser.add_argument(
- "-a", "--addrs", metavar="ADDRS",
- dest="vol_addresses", type=ca_addr_strs_from_csv, action="store",
- help=("Identify objects at indicated volume offsets."))
- parser.add_argument(
- "-b", "--brief-fat",
- dest="show_brief_fat", action="store_true",
- help="Display a summarised FAT cluster map.")
- parser.add_argument(
- "-c", "--claims",
- dest="show_claims", action="store_true",
- help="Show secondary claims on each file.")
- parser.add_argument(
- "-B", "--brief-fancy",
- dest="show_fancy_brief_fat", action="store_true",
- help="Display starts and loops in FAT map.")
- parser.add_argument(
- "-d", "--directory",
- dest="dir_as_file", action="store_true",
- help="Select a directory, not its contents.")
- parser.add_argument(
- "-e", "--errors",
- dest="show_errors", action="store_true",
- help="Show errors.")
- parser.add_argument(
- "-E", "--only-errors",
- dest="only_errors", action="store_true",
- help="Show only errors.")
- parser.add_argument(
- "-f", "--fat",
- dest="show_fat", action="store_true",
- help="Display the FAT cluster entries.")
- parser.add_argument(
- "-h", "--help",
- dest="help", action="store_true",
- help="Display this message and exit.")
- parser.add_argument(
- "-i", "--index",
- dest="show_index", action="store_true",
- help="List IDs generated in traversal.")
- parser.add_argument(
- "-I", "--id", metavar="ID",
- dest="alloc_id", type=int, default=None, action="store",
- help="Select a file or directory by its ID.")
- parser.add_argument(
- "-j", "--junk",
- dest="show_junk", action="store_true",
- help="Show junk entries in directories.")
- parser.add_argument(
- "-l", "--list",
- dest="show_list", action="store_true",
- help="List items (recursively with -r or -R).")
- parser.add_argument(
- "-m", "--metrics",
- dest="show_metrics", action="store_true",
- help="Display metrics and volume information.")
- parser.add_argument(
- "-p", "--path", metavar="PATH",
- dest="pathname", type=str, action="store",
- help=("Select path for list. (See -l)"))
- parser.add_argument(
- "-R", "--recursive",
- dest="recursive", action="store_true",
- help="Recurse through subdirectories.")
- parser.add_argument(
- "-r", "--nested",
- dest="nested", action="store_true",
- help="Recurse with nested indents.")
- parser.add_argument(
- "-s", "--spans",
- dest="show_spans", action="store_true",
- help="Show cluster spans.")
- parser.add_argument(
- "-t", "--table", metavar="N",
- dest="fat_index", type=int, default=0, action="store",
- help="Select the File Allocation Table. (Default = 0)")
- parser.add_argument(
- "-u", "--vol-usage",
- dest="show_vol_usage", action="store_true",
- help="Show counts of used and free clusters.")
- parser.add_argument(
- "-v", "--verbose",
- dest="verbose", action="store_true",
- help="Disable elision of repeats.")
- parser.add_argument(
- "-V", "--version",
- dest="version", action="store_true",
- help="Display version and exit.")
- parser.add_argument(
- "-w", "--width", metavar="W",
- dest="display_width", type=int, default=80, action="store",
- help="Set the output width in characters.")
- parser.add_argument(
- "-x", "--hex",
- dest="display_hex", action="store_true",
- help="Display byte sizes and offsets in hexadecimal.")
- parser.add_argument(
- "-z", "--orphans",
- dest="orphans", action="store_true",
- help="Select orphans in cluster maps.")
- parser.add_argument(
- "fat_image_filename", metavar="VOL-IMAGE",
- type=str,
- help=("Indicate the FAT12/16 volume image to read."))
- if "-h" in sys.argv or "--help" in sys.argv:
- parser.print_help()
- print(
- "\nExamples:\n"
- + " " + cmd + " -lre doom.fat12\n"
- + " " + cmd + " -bu doom.fat12\n"
- + " " + cmd + " -lrjsec doom.fat12\n"
- )
- sys.exit(0)
- if "-V" in sys.argv or "--version" in sys.argv:
- print(VERSION)
- sys.exit(0)
- args = parser.parse_args()
- return args
- #----------------------------------------------------------------------------
- result = 0
- err_msg = ''
- cmd = os.path.basename(sys.argv[0])
- try:
- args = get_arguments()
- img_file = open(args.fat_image_filename, "rb")
- try:
- if args.pathname is not None and args.alloc_id is not None:
- raise ArgError("Cannot select by pathname and ID at the same time.")
- part_offset = 0
- ca_addr_strs = []
- if args.vol_addresses is not None:
- ca_addr_strs = args.vol_addresses
- m = None
- fat = None
- root_dir = None
- id_map = None
- id_list = None
- selected_dir = None
- selected_file = None
- given_id = None
- sel_ids = None
- data_clusters_only = False
- # Oddly, the nested style is more natural in the case of a
- # non-recursive directory listing. This is because in the flat
- # kind of listing, the starting directory requires header text
- # in the same form as the subdirectories.
- recursive = (args.nested or args.recursive) and not args.dir_as_file
- nested = args.nested or not recursive
- # Decide which of the above resources are needed.
- do_walk = (
- args.show_list
- or args.show_index
- or args.show_vol_usage
- or len(ca_addr_strs) > 0
- or args.show_fancy_brief_fat
- or args.pathname is not None
- or args.alloc_id is not None
- or args.orphans
- )
- do_get_fat = do_walk or args.show_fat or args.show_brief_fat
- do_get_metrics = do_get_fat or args.show_metrics
- # Fetch the needed resources.
- if do_get_metrics:
- img_file.seek(part_offset)
- m = FATVolumeMetrics(img_file.read(512))
- if do_get_fat:
- if not (0 <= args.fat_index < m.num_fats):
- raise ArgError("FAT index is out of range.")
- img_file.seek(
- part_offset + m.fat_offset + args.fat_index * m.fat_size)
- fat = get_fat(m, img_file.read(m.fat_size))
- if do_walk:
- root_dir, id_map, id_list = walk_fat(
- m, fat, img_file, part_offset
- )
- if args.pathname is not None:
- # Normalise the path so that an empty string means
- # "Select the contents of the root directory".
- S = args.pathname if args.pathname != "" else "/"
- # Normalise the path further by removing any slash
- # at the beginning that is not also at the end.
- if len(S) >= 2 and S[0] == "/" and S[1] != "/":
- S = S[1:]
- # Because the root directory entry has an empty name string,
- # the search function will need an un-normalised root path.
- #
- # Given that directory-not-its-contents option is available,
- # There is no need to be clever with interpreting a trailing
- # slash on the path.
- f = root_dir.find("" if S == "/" else S)
- if f is not None:
- given_id = f.alloc_id
- else:
- raise ArgError('Cannot find "{}".'.format(args.pathname))
- if args.alloc_id is not None:
- if not 0 <= args.alloc_id < len(id_list):
- raise ArgError("ID {} not found.".format(args.alloc_id))
- given_id = args.alloc_id
- if given_id is not None:
- f = id_list[given_id]
- if f is root_dir and args.dir_as_file:
- raise ArgError("Cannot select the root directory as a file.")
- if args.dir_as_file or not f.is_dir:
- # A single object is being addressed.
- sel_ids = [f.alloc_id]
- selected_file = f
- selected_dir = id_list[selected_file.owner_id]
- else:
- # A directory's contents is being addressed.
- selected_file = None
- selected_dir = f
- sel_ids = []
- if recursive:
- for f in all_dirs_and_files(selected_dir):
- if f is not selected_dir:
- sel_ids.append(f.alloc_id)
- else:
- for f in selected_dir.subdirs:
- sel_ids.append(f.alloc_id)
- for f in selected_dir.files:
- sel_ids.append(f.alloc_id)
- data_clusters_only = True
- if args.orphans:
- if sel_ids is None:
- sel_ids = [0]
- elif 0 not in sel_ids:
- sel_ids.append(0)
- data_clusters_only = True
- # Produce the required output, hopefully with the resources loaded.
- if args.show_metrics:
- printlines(metrics_report(m))
- if args.show_fat:
- w = nice_num_columns(args.display_width, m.cfw + 1, 1 + m.cfw)
- if sel_ids is None:
- printlines(cluster_report(
- m, fat,
- data_clusters_only,
- not args.verbose, w))
- else:
- printlines(selective_cluster_report(
- m, fat,
- data_clusters_only, sel_ids,
- id_map,
- not args.verbose, w))
- if args.show_brief_fat or args.show_fancy_brief_fat:
- w = nice_num_columns(args.display_width, m.cfw + 2, 1)
- if args.show_fancy_brief_fat or sel_ids is not None:
- printlines(fancy_brief_map_report(
- m, fat, root_dir,
- data_clusters_only, sel_ids,
- id_map, not args.verbose, w,
- args.show_fancy_brief_fat))
- else:
- printlines(brief_map_report(
- m, fat,
- data_clusters_only,
- not args.verbose, w))
- if args.show_list:
- d = selected_dir if selected_dir is not None else root_dir
- opts = 0x00
- opts |= DIR_SHOW_FLAT if recursive and not nested else 0
- opts |= DIR_SHOW_HEX if args.display_hex else 0
- opts |= DIR_SHOW_SPANS if args.show_spans else 0
- opts |= DIR_SHOW_JUNK if args.show_junk else 0
- opts |= DIR_SHOW_CLAIMS if args.show_claims else 0
- opts |= DIR_SHOW_ERRORS if args.show_errors else 0
- opts |= DIR_SHOW_ONLY_ERRORS if args.only_errors else 0
- printlines(dir_report(
- m, d, selected_file, opts,
- id_map, id_list,
- 0, None if recursive else 0,
- " "))
- if args.show_index:
- printlines(index_report(m, root_dir, id_list))
- if args.show_vol_usage:
- usage_stats = get_fat_usage(m, fat, id_map)
- printlines(fat_usage_report(m, usage_stats))
- if not args.show_list:
- if selected_file is not None:
- f = selected_file
- else:
- f = selected_dir
- if f is not None:
- err_prefix = ""
- if args.show_claims:
- printlines(secondary_claims_report(
- m, f, id_map, id_list, args.display_hex))
- if args.show_errors or args.only_errors:
- for e in f.errors:
- print("{}{}".format(err_prefix, str(e)))
- if args.show_spans:
- printlines(spans_report(
- m, f.clusters, f.size, f.collision_cluster))
- if len(ca_addr_strs) > 0:
- for ca_addr_str in ca_addr_strs:
- if ca_addr_str[0] == "C":
- c = int(ca_addr_str[1:])
- addr = m.da_offset + m.cluster_size * (c - m.min_c)
- else:
- addr = int(ca_addr_str[1:])
- printlines(addr_report(
- m, addr, fat, id_map, id_list, args.display_hex))
- finally:
- img_file.close()
- except ArgError as E:
- err_msg = 'Error: ' + str(E)
- result = 2
- except FileError as E:
- err_msg = str(E)
- result = 3
- except CmdError as E:
- err_msg = str(E)
- result = 4
- except DataError as E:
- err_msg = str(E)
- result = 5
- except Exception as E:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- err_lines = traceback.format_exc().splitlines()
- err_msg = 'Unhandled exception:\n' + '\n'.join(err_lines)
- result = 1
- if err_msg != '':
- print(cmd + ': ' + err_msg, file=sys.stderr)
- return result
- #------------------------------------------------------------------------------
- # Command line trigger
- #------------------------------------------------------------------------------
- if __name__ == '__main__':
- sys.exit(main())
- #------------------------------------------------------------------------------
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement