diff --git a/main.py b/main.py
index f523ae5..07de873 100755
--- a/main.py
+++ b/main.py
@@ -30,6 +30,7 @@
import sys
try:
import readline
+ readline # ignore unused import warning
except ImportError:
pass
@@ -37,6 +38,10 @@
# scanners
from recuperabit.fs.ntfs import NTFSScanner
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from recuperabit.fs.core_types import Partition
+
__author__ = "Andrea Lazzarotto"
__copyright__ = "(c) 2014-2021, Andrea Lazzarotto"
__license__ = "GPLv3"
@@ -97,7 +102,7 @@ def check_valid_part(num, parts, shorthands, rebuild=True):
return None
-def interpret(cmd, arguments, parts, shorthands, outdir):
+def interpret(cmd, arguments, parts: dict[int, 'Partition'], shorthands, outdir):
"""Perform command required by user."""
if cmd == 'help':
print('Available commands:')
@@ -362,7 +367,7 @@ def main():
pickle.dump(interesting, savefile)
# Ask for partitions
- parts = {}
+ parts: dict[int, Partition] = {}
for scanner in scanners:
parts.update(scanner.get_partitions())
diff --git a/recuperabit/fs/constants.py b/recuperabit/fs/constants.py
index 9370a77..69c928d 100644
--- a/recuperabit/fs/constants.py
+++ b/recuperabit/fs/constants.py
@@ -19,5 +19,5 @@
# along with RecuperaBit. If not, see .
-sector_size = 512
-max_sectors = 256 # Maximum block size for recovery
+sector_size: int = 512
+max_sectors: int = 256 # Maximum block size for recovery
diff --git a/recuperabit/fs/core_types.py b/recuperabit/fs/core_types.py
index 87dda78..eb94d2a 100644
--- a/recuperabit/fs/core_types.py
+++ b/recuperabit/fs/core_types.py
@@ -25,6 +25,8 @@
import logging
import os.path
+from typing import Optional, Dict, Set, List, Tuple, Union, Any, Iterator
+from datetime import datetime
from .constants import sector_size
@@ -32,49 +34,49 @@
class File(object):
- """Filesystem-independent representation of a file."""
- def __init__(self, index, name, size, is_directory=False,
- is_deleted=False, is_ghost=False):
- self.index = index
- self.name = name
- self.size = size
- self.is_directory = is_directory
- self.is_deleted = is_deleted
- self.is_ghost = is_ghost
- self.parent = None
- self.mac = {
+ """Filesystem-independent representation of a file. Aka Node."""
+ def __init__(self, index: Union[int, str], name: str, size: Optional[int], is_directory: bool = False,
+ is_deleted: bool = False, is_ghost: bool = False) -> None:
+ self.index: Union[int, str] = index
+ self.name: str = name
+ self.size: Optional[int] = size
+ self.is_directory: bool = is_directory
+ self.is_deleted: bool = is_deleted
+ self.is_ghost: bool = is_ghost
+ self.parent: Optional[Union[int, str]] = None
+ self.mac: Dict[str, Optional[datetime]] = {
'modification': None,
'access': None,
'creation': None
}
- self.children = set()
- self.children_names = set() # Avoid name clashes breaking restore
- self.offset = None # Offset from beginning of disk
+ self.children: Set['File'] = set()
+ self.children_names: Set[str] = set() # Avoid name clashes breaking restore
+ self.offset: Optional[int] = None # Offset from beginning of disk
- def set_parent(self, parent):
+ def set_parent(self, parent: Optional[Union[int, str]]) -> None:
"""Set a pointer to the parent directory."""
self.parent = parent
- def set_mac(self, modification, access, creation):
+ def set_mac(self, modification: Optional[datetime], access: Optional[datetime], creation: Optional[datetime]) -> None:
"""Set the modification, access and creation times."""
self.mac['modification'] = modification
self.mac['access'] = access
self.mac['creation'] = creation
- def get_mac(self):
+ def get_mac(self) -> List[Optional[datetime]]:
"""Get the modification, access and creation times."""
keys = ('modification', 'access', 'creation')
return [self.mac[k] for k in keys]
- def set_offset(self, offset):
+ def set_offset(self, offset: Optional[int]) -> None:
"""Set the offset of the file record with respect to the disk image."""
self.offset = offset
- def get_offset(self):
+ def get_offset(self) -> Optional[int]:
"""Get the offset of the file record with respect to the disk image."""
return self.offset
- def add_child(self, node):
+ def add_child(self, node: 'File') -> None:
"""Add a new child to this directory."""
original_name = node.name
i = 0
@@ -90,7 +92,7 @@ def add_child(self, node):
self.children.add(node)
self.children_names.add(node.name)
- def full_path(self, part):
+ def full_path(self, part: 'Partition') -> str:
"""Return the full path of this file."""
if self.parent is not None:
parent = part[self.parent]
@@ -98,7 +100,7 @@ def full_path(self, part):
else:
return self.name
- def get_content(self, partition):
+ def get_content(self, partition: 'Partition') -> Optional[Union[bytes, Iterator[bytes]]]:
# pylint: disable=W0613
"""Extract the content of the file.
@@ -109,14 +111,14 @@ def get_content(self, partition):
raise NotImplementedError
# pylint: disable=R0201
- def ignore(self):
+ def ignore(self) -> bool:
"""The following method is used by the restore procedure to check
files that should not be recovered. For example, in NTFS file
$BadClus:$Bad shall not be recovered because it creates an output
with the same size as the partition (usually many GBs)."""
return False
- def __repr__(self):
+ def __repr__(self) -> str:
return (
u'File(#%s, ^^%s^^, %s, offset = %s sectors)' %
(self.index, self.parent, self.name, self.offset)
@@ -128,42 +130,42 @@ class Partition(object):
Parameter root_id represents the identifier assigned to the root directory
of a partition. This can be file system dependent."""
- def __init__(self, fs_type, root_id, scanner):
- self.fs_type = fs_type
- self.root_id = root_id
- self.size = None
- self.offset = None
- self.root = None
- self.lost = File(-1, 'LostFiles', 0, is_directory=True, is_ghost=True)
- self.files = {}
- self.recoverable = False
- self.scanner = scanner
-
- def add_file(self, node):
+ def __init__(self, fs_type: str, root_id: Union[int, str], scanner: 'DiskScanner') -> None:
+ self.fs_type: str = fs_type
+ self.root_id: Union[int, str] = root_id
+ self.size: Optional[int] = None
+ self.offset: Optional[int] = None
+ self.root: Optional[File] = None
+ self.lost: File = File(-1, 'LostFiles', 0, is_directory=True, is_ghost=True)
+ self.files: Dict[Union[int, str], File] = {}
+ self.recoverable: bool = False
+ self.scanner: 'DiskScanner' = scanner
+
+ def add_file(self, node: File) -> None:
"""Insert a new file in the partition."""
index = node.index
self.files[index] = node
- def set_root(self, node):
+ def set_root(self, node: File) -> None:
"""Set the root directory."""
if not node.is_directory:
raise TypeError('Not a directory')
self.root = node
self.root.set_parent(None)
- def set_size(self, size):
+ def set_size(self, size: int) -> None:
"""Set the (estimated) size of the partition."""
self.size = size
- def set_offset(self, offset):
+ def set_offset(self, offset: int) -> None:
"""Set the offset from the beginning of the disk."""
self.offset = offset
- def set_recoverable(self, recoverable):
+ def set_recoverable(self, recoverable: bool) -> None:
"""State if the partition contents are also recoverable."""
self.recoverable = recoverable
- def rebuild(self):
+ def rebuild(self) -> None:
"""Rebuild the partition structure.
This method processes the contents of files and it rebuilds the
@@ -201,11 +203,11 @@ def rebuild(self):
return
# pylint: disable=R0201
- def additional_repr(self):
+ def additional_repr(self) -> List[Tuple[str, Any]]:
"""Return additional values to show in the string representation."""
return []
- def __repr__(self):
+ def __repr__(self) -> str:
size = (
readable_bytes(self.size * sector_size)
if self.size is not None else '??? b'
@@ -227,14 +229,14 @@ def __repr__(self):
', '.join(a+': '+str(b) for a, b in data)
)
- def __getitem__(self, index):
+ def __getitem__(self, index: Union[int, str]) -> File:
if index in self.files:
return self.files[index]
if index == self.lost.index:
return self.lost
raise KeyError
- def get(self, index, default=None):
+ def get(self, index: Union[int, str], default: Optional[File] = None) -> Optional[File]:
"""Get a file or the special LostFiles directory."""
try:
return self.__getitem__(index)
@@ -244,17 +246,22 @@ def get(self, index, default=None):
class DiskScanner(object):
"""Abstract stub for the implementation of disk scanners."""
- def __init__(self, pointer):
- self.image = pointer
+ def __init__(self, pointer: Any) -> None:
+ self.image: Any = pointer
- def get_image(self):
+ def get_image(self) -> Any:
"""Return the image reference."""
return self.image
- def feed(self, index, sector):
+ @staticmethod
+ def get_image(scanner: 'DiskScanner') -> Any:
+ """Static method to get image from scanner instance."""
+ return scanner.image
+
+ def feed(self, index: int, sector: bytes) -> Optional[str]:
"""Feed a new sector."""
raise NotImplementedError
- def get_partitions(self):
+ def get_partitions(self) -> Dict[int, Partition]:
"""Get a list of the found partitions."""
raise NotImplementedError
diff --git a/recuperabit/fs/ntfs.py b/recuperabit/fs/ntfs.py
index f0e820c..a168404 100644
--- a/recuperabit/fs/ntfs.py
+++ b/recuperabit/fs/ntfs.py
@@ -24,6 +24,7 @@
import logging
from collections import Counter
+from typing import Any, Dict, List, Optional, Tuple, Union, Iterator, Set
from .constants import max_sectors, sector_size
from .core_types import DiskScanner, File, Partition
@@ -36,7 +37,7 @@
from ..utils import merge, sectors, unpack
# Some attributes may appear multiple times
-multiple_attributes = set([
+multiple_attributes: Set[str] = set([
'$FILE_NAME',
'$DATA',
'$INDEX_ROOT',
@@ -45,11 +46,11 @@
])
# Size of records in sectors
-FILE_size = 2
-INDX_size = 8
+FILE_size: int = 2
+INDX_size: int = 8
-def best_name(entries):
+def best_name(entries: List[Tuple[int, str]]) -> Optional[str]:
"""Return the best file name available.
This function accepts a list of tuples formed by a namespace and a string.
@@ -66,8 +67,10 @@ def best_name(entries):
return name if len(name) else None
-def parse_mft_attr(attr):
+def parse_mft_attr(attr: Union[bytes, bytearray]) -> Tuple[Dict[str, Any], Optional[str]]:
"""Parse the contents of a MFT attribute."""
+ assert isinstance(attr, (bytes, bytearray)), f"attr must be bytes or bytearray, got {type(attr)}"
+
header = unpack(attr, attr_header_fmt)
attr_type = header['type']
@@ -94,7 +97,7 @@ def parse_mft_attr(attr):
return header, name
-def _apply_fixup_values(header, entry):
+def _apply_fixup_values(header: Dict[str, Any], entry: bytearray) -> None:
"""Apply the fixup values to FILE and INDX records."""
offset = header['off_fixup']
for i in range(1, header['n_entries']):
@@ -102,7 +105,7 @@ def _apply_fixup_values(header, entry):
entry[pos-2:pos] = entry[offset + 2*i:offset + 2*(i+1)]
-def _attributes_reader(entry, offset):
+def _attributes_reader(entry: Union[bytes, bytearray], offset: int) -> Dict[str, Any]:
"""Read every attribute."""
attributes = {}
while offset < len(entry) - 16:
@@ -133,8 +136,10 @@ def _attributes_reader(entry, offset):
return attributes
-def parse_file_record(entry):
+def parse_file_record(entry: bytearray | bytes) -> Dict[str, Any]:
"""Parse the contents of a FILE record (MFT entry)."""
+ assert isinstance(entry, (bytearray, bytes)), f"entry must be bytearray or bytes, got {type(entry)}"
+
header = unpack(entry, entry_fmt)
if (header['size_alloc'] is None or
header['size_alloc'] > len(entry) or
@@ -154,8 +159,10 @@ def parse_file_record(entry):
return header
-def parse_indx_record(entry):
+def parse_indx_record(entry: bytearray | bytes) -> Dict[str, Any]:
"""Parse the contents of a INDX record (directory index)."""
+ assert isinstance(entry, (bytearray, bytes)), f"entry must be bytearray or bytes, got {type(entry)}"
+
header = unpack(entry, indx_fmt)
_apply_fixup_values(header, entry)
@@ -200,7 +207,7 @@ def parse_indx_record(entry):
return header
-def _integrate_attribute_list(parsed, part, image):
+def _integrate_attribute_list(parsed: Dict[str, Any], part: 'NTFSPartition', image: Any) -> None:
"""Integrate missing attributes in the parsed MTF entry."""
base_record = parsed['record_n']
attrs = parsed['attributes']
@@ -264,7 +271,7 @@ def _integrate_attribute_list(parsed, part, image):
class NTFSFile(File):
"""NTFS File."""
- def __init__(self, parsed, offset, is_ghost=False, ads=''):
+ def __init__(self, parsed: Dict[str, Any], offset: Optional[int], is_ghost: bool = False, ads: str = '') -> None:
index = parsed['record_n']
ads_suffix = ':' + ads if ads != '' else ads
if ads != '':
@@ -322,7 +329,7 @@ def __init__(self, parsed, offset, is_ghost=False, ads=''):
self.ads = ads
@staticmethod
- def _padded_bytes(image, offset, size):
+ def _padded_bytes(image: Any, offset: int, size: int) -> bytes:
dump = sectors(image, offset, size, 1)
if len(dump) < size:
logging.warning(
@@ -331,7 +338,7 @@ def _padded_bytes(image, offset, size):
dump += bytearray(b'\x00' * (size - len(dump)))
return dump
- def content_iterator(self, partition, image, datas):
+ def content_iterator(self, partition: 'NTFSPartition', image: Any, datas: List[Dict[str, Any]]) -> Iterator[bytes]:
"""Return an iterator for the contents of this file."""
vcn = 0
spc = partition.sec_per_clus
@@ -378,7 +385,7 @@ def content_iterator(self, partition, image, datas):
yield bytes(partial)
vcn = attr['end_VCN'] + 1
- def get_content(self, partition):
+ def get_content(self, partition: 'NTFSPartition') -> Optional[Union[bytes, Iterator[bytes]]]:
"""Extract the content of the file.
This method works by extracting the $DATA attribute."""
@@ -439,7 +446,7 @@ def get_content(self, partition):
)
return self.content_iterator(partition, image, non_resident)
- def ignore(self):
+ def ignore(self) -> bool:
"""Determine which files should be ignored."""
return (
(self.index == '8:$Bad') or
@@ -449,13 +456,13 @@ def ignore(self):
class NTFSPartition(Partition):
"""Partition with additional fields for NTFS recovery."""
- def __init__(self, scanner, position=None):
+ def __init__(self, scanner: 'NTFSScanner', position: Optional[int] = None) -> None:
Partition.__init__(self, 'NTFS', 5, scanner)
- self.sec_per_clus = None
- self.mft_pos = position
- self.mftmirr_pos = None
+ self.sec_per_clus: Optional[int] = None
+ self.mft_pos: Optional[int] = position
+ self.mftmirr_pos: Optional[int] = None
- def additional_repr(self):
+ def additional_repr(self) -> List[Tuple[str, Any]]:
"""Return additional values to show in the string representation."""
return [
('Sec/Clus', self.sec_per_clus),
@@ -466,17 +473,17 @@ def additional_repr(self):
class NTFSScanner(DiskScanner):
"""NTFS Disk Scanner."""
- def __init__(self, pointer):
+ def __init__(self, pointer: Any) -> None:
DiskScanner.__init__(self, pointer)
- self.found_file = set()
- self.parsed_file_review = {}
- self.found_indx = set()
- self.parsed_indx = {}
- self.indx_list = None
- self.found_boot = []
- self.found_spc = []
-
- def feed(self, index, sector):
+ self.found_file: Set[int] = set()
+ self.parsed_file_review: Dict[int, Dict[str, Any]] = {}
+ self.found_indx: Set[int] = set()
+ self.parsed_indx: Dict[int, Dict[str, Any]] = {}
+ self.indx_list: Optional[SparseList[int]] = None
+ self.found_boot: List[int] = []
+ self.found_spc: List[int] = []
+
+ def feed(self, index: int, sector: bytes) -> Optional[str]:
"""Feed a new sector."""
# check boot sector
if sector.endswith(b'\x55\xAA') and b'NTFS' in sector[:8]:
@@ -494,7 +501,7 @@ def feed(self, index, sector):
return 'NTFS index record'
@staticmethod
- def add_indx_entries(entries, part):
+ def add_indx_entries(entries: List[Dict[str, Any]], part: NTFSPartition) -> None:
"""Insert new ghost files which were not already found."""
for rec in entries:
if (rec['record_n'] not in part.files and
@@ -512,7 +519,7 @@ def add_indx_entries(entries, part):
rec['flags'] = 0x1
part.add_file(NTFSFile(rec, None, is_ghost=True))
- def add_from_indx_root(self, parsed, part):
+ def add_from_indx_root(self, parsed: Dict[str, Any], part: NTFSPartition) -> None:
"""Add ghost entries to part from INDEX_ROOT attributes in parsed."""
for attribute in parsed['attributes']['$INDEX_ROOT']:
if (attribute.get('content') is None or
@@ -520,7 +527,7 @@ def add_from_indx_root(self, parsed, part):
continue
self.add_indx_entries(attribute['content']['records'], part)
- def most_likely_sec_per_clus(self):
+ def most_likely_sec_per_clus(self) -> List[int]:
"""Determine the most likely value of sec_per_clus of each partition,
to speed up the search."""
counter = Counter()
@@ -528,7 +535,7 @@ def most_likely_sec_per_clus(self):
counter.update(2**i for i in range(8))
return [i for i, _ in counter.most_common()]
- def find_boundary(self, part, mft_address, multipliers):
+ def find_boundary(self, part: NTFSPartition, mft_address: int, multipliers: List[int]) -> Tuple[Optional[int], Optional[int]]:
"""Determine the starting sector of a partition with INDX records."""
nodes = (
self.parsed_file_review[node.offset]
@@ -593,7 +600,7 @@ def find_boundary(self, part, mft_address, multipliers):
else:
return (None, None)
- def add_from_indx_allocation(self, parsed, part):
+ def add_from_indx_allocation(self, parsed: Dict[str, Any], part: NTFSPartition) -> None:
"""Add ghost entries to part from INDEX_ALLOCATION attributes in parsed.
This procedure requires that the beginning of the partition has already
@@ -625,7 +632,7 @@ def add_from_indx_allocation(self, parsed, part):
entries = parse_indx_record(dump)['entries']
self.add_indx_entries(entries, part)
- def add_from_attribute_list(self, parsed, part, offset):
+ def add_from_attribute_list(self, parsed: Dict[str, Any], part: NTFSPartition, offset: int) -> None:
"""Add additional entries to part from attributes in ATTRIBUTE_LIST.
Files with many attributes may have additional attributes not in the
@@ -643,7 +650,7 @@ def add_from_attribute_list(self, parsed, part, offset):
if ads_name and len(ads_name):
part.add_file(NTFSFile(parsed, offset, ads=ads_name))
- def add_from_mft_mirror(self, part):
+ def add_from_mft_mirror(self, part: NTFSPartition) -> None:
"""Fix the first file records using the MFT mirror."""
img = DiskScanner.get_image(self)
mirrpos = part.mftmirr_pos
@@ -664,7 +671,7 @@ def add_from_mft_mirror(self, part):
'%s from backup', node.index, node.name, part.offset
)
- def finalize_reconstruction(self, part):
+ def finalize_reconstruction(self, part: NTFSPartition) -> None:
"""Finish information gathering from a file.
This procedure requires that the beginning of the
@@ -693,9 +700,9 @@ def finalize_reconstruction(self, part):
parsed = self.parsed_file_review[node.offset]
self.add_from_indx_allocation(parsed, part)
- def get_partitions(self):
+ def get_partitions(self) -> Dict[int, NTFSPartition]:
"""Get a list of the found partitions."""
- partitioned_files = {}
+ partitioned_files: Dict[int, NTFSPartition] = {}
img = DiskScanner.get_image(self)
logging.info('Parsing MFT entries')
diff --git a/recuperabit/logic.py b/recuperabit/logic.py
index e97052b..c4f10c2 100644
--- a/recuperabit/logic.py
+++ b/recuperabit/logic.py
@@ -27,30 +27,34 @@
import sys
import time
import types
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, Iterator, Set, Tuple, TypeVar, Generic
-from .utils import tiny_repr
+T = TypeVar('T')
+if TYPE_CHECKING:
+ from .fs.core_types import File, Partition
-class SparseList(object):
+
+class SparseList(Generic[T]):
"""List which only stores values at some places."""
- def __init__(self, data=None, default=None):
- self.keys = [] # This is always kept in order
- self.elements = {}
- self.default = default
+ def __init__(self, data: Optional[Dict[int, T]] = None, default: Optional[T] = None) -> None:
+ self.keys: List[int] = [] # This is always kept in order
+ self.elements: Dict[int, T] = {}
+ self.default: Optional[T] = default
if data is not None:
self.keys = sorted(data)
self.elements.update(data)
- def __len__(self):
+ def __len__(self) -> int:
try:
return self.keys[-1] + 1
except IndexError:
return 0
- def __getitem__(self, index):
+ def __getitem__(self, index: int) -> Optional[T]:
return self.elements.get(index, self.default)
- def __setitem__(self, index, item):
+ def __setitem__(self, index: int, item: T) -> None:
if item == self.default:
if index in self.elements:
del self.elements[index]
@@ -60,18 +64,18 @@ def __setitem__(self, index, item):
bisect.insort(self.keys, index)
self.elements[index] = item
- def __contains__(self, element):
+ def __contains__(self, element: T) -> bool:
return element in self.elements.values()
- def __iter__(self):
+ def __iter__(self) -> Iterator[int]:
return self.keys.__iter__()
- def __repr__(self):
+ def __repr__(self) -> str:
elems = []
prevk = 0
if len(self.elements) > 0:
k = self.keys[0]
- elems.append(str(k) + ' -> ' + tiny_repr(self.elements[k]))
+ elems.append(str(k) + ' -> ' + repr(self.elements[k]))
prevk = self.keys[0]
for i in range(1, len(self.elements)):
nextk = self.keys[i]
@@ -79,31 +83,31 @@ def __repr__(self):
while prevk < nextk - 1:
elems.append('__')
prevk += 1
- elems.append(tiny_repr(self.elements[nextk]))
+ elems.append(repr(self.elements[nextk]))
else:
elems.append('\n... ' + str(nextk) + ' -> ' +
- tiny_repr(self.elements[nextk]))
+ repr(self.elements[nextk]))
prevk = nextk
return '[' + ', '.join(elems) + ']'
- def iterkeys(self):
+ def iterkeys(self) -> Iterator[int]:
"""An iterator over the keys of actual elements."""
return self.__iter__()
- def iterkeys_rev(self):
+ def iterkeys_rev(self) -> Iterator[int]:
"""An iterator over the keys of actual elements (reversed)."""
i = len(self.keys)
while i > 0:
i -= 1
yield self.keys[i]
- def itervalues(self):
+ def itervalues(self) -> Iterator[T]:
"""An iterator over the elements."""
for k in self.keys:
yield self.elements[k]
- def wipe_interval(self, bottom, top):
+ def wipe_interval(self, bottom: int, top: int) -> None:
"""Remove elements between bottom and top."""
new_keys = set()
if bottom > top:
@@ -121,12 +125,12 @@ def wipe_interval(self, bottom, top):
self.keys = sorted(new_keys)
-def preprocess_pattern(pattern):
+def preprocess_pattern(pattern: SparseList[T]) -> Dict[T, List[int]]:
"""Preprocess a SparseList for approximate string matching.
This function performs preprocessing for the Baeza-Yates--Perleberg
fast and practical approximate string matching algorithm."""
- result = {}
+ result: Dict[T, List[int]] = {}
length = pattern.__len__()
for k in pattern:
name = pattern[k]
@@ -137,7 +141,7 @@ def preprocess_pattern(pattern):
return result
-def approximate_matching(records, pattern, stop, k=1):
+def approximate_matching(records: SparseList[T], pattern: SparseList[T], stop: int, k: int = 1) -> Optional[List[Union[Set[int], int, float]]]:
"""Find the best match for a given pattern.
The Baeza-Yates--Perleberg algorithm requires a preprocessed pattern. This
@@ -152,8 +156,8 @@ def approximate_matching(records, pattern, stop, k=1):
return None
lookup = preprocess_pattern(pattern)
- count = SparseList(default=0)
- match_offsets = set()
+ count: SparseList[int] = SparseList(default=0)
+ match_offsets: Set[int] = set()
i = 0
j = 0 # previous value of i
@@ -192,7 +196,7 @@ def approximate_matching(records, pattern, stop, k=1):
return None
-def makedirs(path):
+def makedirs(path: str) -> bool:
"""Make directories if they do not exist."""
try:
os.makedirs(path)
@@ -205,7 +209,7 @@ def makedirs(path):
return True
-def recursive_restore(node, part, outputdir, make_dirs=True):
+def recursive_restore(node: 'File', part: 'Partition', outputdir: str, make_dirs: bool = True) -> None:
"""Restore a directory structure starting from a file node."""
parent_path = str(
part[node.parent].full_path(part) if node.parent is not None
diff --git a/recuperabit/utils.py b/recuperabit/utils.py
index 3ee1424..45baaa5 100644
--- a/recuperabit/utils.py
+++ b/recuperabit/utils.py
@@ -19,25 +19,31 @@
# along with RecuperaBit. If not, see .
+from datetime import datetime
import logging
import pprint
import string
import sys
import time
+from typing import TYPE_CHECKING, Any, Optional, List, Dict, Tuple, Union, Callable
import unicodedata
+import io
from .fs.constants import sector_size
-printer = pprint.PrettyPrinter(indent=4)
+printer: pprint.PrettyPrinter = pprint.PrettyPrinter(indent=4)
all_chars = (chr(i) for i in range(sys.maxunicode))
-unicode_printable = set(
+unicode_printable: set[str] = set(
c for c in all_chars
if not unicodedata.category(c)[0].startswith('C')
)
-ascii_printable = set(string.printable[:-5])
+ascii_printable: set[str] = set(string.printable[:-5])
+if TYPE_CHECKING:
+ from .fs.core_types import File, Partition
-def sectors(image, offset, size, bsize=sector_size, fill=True):
+
+def sectors(image: io.BufferedReader, offset: int, size: int, bsize: int = sector_size, fill: bool = True) -> Optional[bytearray]:
"""Read from a file descriptor."""
read = True
try:
@@ -60,7 +66,7 @@ def sectors(image, offset, size, bsize=sector_size, fill=True):
return None
return bytearray(dump)
-def unixtime(dtime):
+def unixtime(dtime: Optional[datetime]) -> int:
"""Convert datetime to UNIX epoch."""
if dtime is None:
return 0
@@ -72,9 +78,9 @@ def unixtime(dtime):
# format:
# [(label, (formatter, lower, higher)), ...]
-def unpack(data, fmt):
+def unpack(data: bytes | bytearray, fmt: List[Tuple[str, Tuple[Union[str, Callable[[bytes], Any]], Union[int, Callable[[Dict[str, Any]], Optional[int]]], Union[int, Callable[[Dict[str, Any]], Optional[int]]]]]]) -> Dict[str, Any]:
"""Extract formatted information from a string of bytes."""
- result = {}
+ result: Dict[str, Any] = {}
for label, description in fmt:
formatter, lower, higher = description
# If lower is a function, then apply it
@@ -112,9 +118,9 @@ def unpack(data, fmt):
return result
-def feed_all(image, scanners, indexes):
+def feed_all(image: io.BufferedReader, scanners: List[Any], indexes: List[int]) -> List[int]:
# Scan the disk image and feed the scanners
- interesting = []
+ interesting: List[int] = []
for index in indexes:
sector = sectors(image, index, 1, fill=False)
if not sector:
@@ -128,29 +134,19 @@ def feed_all(image, scanners, indexes):
return interesting
-def printable(text, default='.', alphabet=None):
+def printable(text: str, default: str = '.', alphabet: Optional[set[str]] = None) -> str:
"""Replace unprintable characters in a text with a default one."""
if alphabet is None:
alphabet = unicode_printable
return ''.join((i if i in alphabet else default) for i in text)
-def pretty(dictionary):
- """Format dictionary with the pretty printer."""
- return printer.pformat(dictionary)
-def show(dictionary):
- """Print dictionary with the pretty printer."""
- printer.pprint(dictionary)
-def tiny_repr(element):
- """deprecated: Return a representation of unicode strings without the u."""
- rep = repr(element)
- return rep[1:] if type(element) == unicode else rep
-def readable_bytes(amount):
+def readable_bytes(amount: Optional[int]) -> str:
"""Return a human readable string representing a size in bytes."""
if amount is None:
return '??? B'
@@ -164,7 +160,7 @@ def readable_bytes(amount):
return '%.2f %sB' % (scaled, powers[biggest])
-def _file_tree_repr(node):
+def _file_tree_repr(node: 'File') -> str:
"""Give a nice representation for the tree."""
desc = (
' [GHOST]' if node.is_ghost else
@@ -188,9 +184,9 @@ def _file_tree_repr(node):
)
-def tree_folder(directory, padding=0):
+def tree_folder(directory: 'File', padding: int = 0) -> str:
"""Return a tree-like textual representation of a directory."""
- lines = []
+ lines: List[str] = []
pad = ' ' * padding
lines.append(
pad + _file_tree_repr(directory)
@@ -207,7 +203,7 @@ def tree_folder(directory, padding=0):
return '\n'.join(lines)
-def _bodyfile_repr(node, path):
+def _bodyfile_repr(node: 'File', path: str) -> str:
"""Return a body file line for node."""
end = '/' if node.is_directory or len(node.children) else ''
return '|'.join(str(el) for el in [
@@ -223,13 +219,13 @@ def _bodyfile_repr(node, path):
])
-def bodyfile_folder(directory, path=''):
+def bodyfile_folder(directory: 'File', path: str = '') -> List[str]:
"""Create a body file compatible with TSK 3.x.
Format:
'#MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime'
See also: http://wiki.sleuthkit.org/index.php?title=Body_file"""
- lines = [_bodyfile_repr(directory, path)]
+ lines: List[str] = [_bodyfile_repr(directory, path)]
path += directory.name + '/'
for entry in directory.children:
if len(entry.children) or entry.is_directory:
@@ -239,7 +235,7 @@ def bodyfile_folder(directory, path=''):
return lines
-def _ltx_clean(label):
+def _ltx_clean(label: Any) -> str:
"""Small filter to prepare strings to be included in LaTeX code."""
clean = str(label).replace('$', r'\$').replace('_', r'\_')
if clean[0] == '-':
@@ -247,7 +243,7 @@ def _ltx_clean(label):
return clean
-def _tikz_repr(node):
+def _tikz_repr(node: 'File') -> str:
"""Represent the node for a Tikz diagram."""
return r'node %s{%s\enskip{}%s}' % (
'[ghost]' if node.is_ghost else '[deleted]' if node.is_deleted else '',
@@ -255,11 +251,11 @@ def _tikz_repr(node):
)
-def tikz_child(directory, padding=0):
+def tikz_child(directory: 'File', padding: int = 0) -> Tuple[str, int]:
"""Write a child row for Tikz representation."""
pad = ' ' * padding
- lines = [r'%schild {%s' % (pad, _tikz_repr(directory))]
- count = len(directory.children)
+ lines: List[str] = [r'%schild {%s' % (pad, _tikz_repr(directory))]
+ count: int = len(directory.children)
for entry in directory.children:
content, number = tikz_child(entry, padding+4)
lines.append(content)
@@ -270,7 +266,7 @@ def tikz_child(directory, padding=0):
return '\n'.join(lines).replace('\n}', '}'), count
-def tikz_part(part):
+def tikz_part(part: 'Partition') -> str:
"""Create LaTeX code to represent the directory structure as a nice Tikz
diagram.
@@ -296,7 +292,7 @@ def tikz_part(part):
)
-def csv_part(part):
+def csv_part(part: 'Partition') -> list[str]:
"""Provide a CSV representation for a partition."""
contents = [
','.join(('Id', 'Parent', 'Name', 'Full Path', 'Modification Time',
@@ -324,9 +320,9 @@ def csv_part(part):
return contents
-def _sub_locate(text, directory, part):
+def _sub_locate(text: str, directory: 'File', part: 'Partition') -> List[Tuple['File', str]]:
"""Helper for locate."""
- lines = []
+ lines: List[Tuple['File', str]] = []
for entry in sorted(directory.children, key=lambda node: node.name):
path = entry.full_path(part)
if text in path.lower():
@@ -336,16 +332,16 @@ def _sub_locate(text, directory, part):
return lines
-def locate(part, text):
+def locate(part: 'Partition', text: str) -> List[Tuple['File', str]]:
"""Return paths of files matching the text."""
- lines = []
+ lines: List[Tuple['File', str]] = []
text = text.lower()
lines += _sub_locate(text, part.lost, part)
lines += _sub_locate(text, part.root, part)
return lines
-def merge(part, piece):
+def merge(part: 'Partition', piece: 'Partition') -> None:
"""Merge piece into part (both are partitions)."""
for index in piece.files:
if (
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..76b86d6
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Test suite for RecuperaBit NTFS recovery tool."""
diff --git a/tests/data/reference_files/deep/nested/directory/deep_file.txt b/tests/data/reference_files/deep/nested/directory/deep_file.txt
new file mode 100644
index 0000000..22ce1eb
--- /dev/null
+++ b/tests/data/reference_files/deep/nested/directory/deep_file.txt
@@ -0,0 +1 @@
+File in deep nested directory
diff --git a/tests/data/reference_files/empty_file.empty b/tests/data/reference_files/empty_file.empty
new file mode 100644
index 0000000..e69de29
diff --git a/tests/data/reference_files/file_with_ads.txt b/tests/data/reference_files/file_with_ads.txt
new file mode 100644
index 0000000..3a0c07f
--- /dev/null
+++ b/tests/data/reference_files/file_with_ads.txt
@@ -0,0 +1 @@
+File with ADS main content
diff --git a/tests/data/reference_files/large_file.dat b/tests/data/reference_files/large_file.dat
new file mode 100644
index 0000000..8f0c060
--- /dev/null
+++ b/tests/data/reference_files/large_file.dat
@@ -0,0 +1 @@
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
\ No newline at end of file
diff --git a/tests/data/reference_files/medium_binary.bin b/tests/data/reference_files/medium_binary.bin
new file mode 100644
index 0000000..a6a8d35
Binary files /dev/null and b/tests/data/reference_files/medium_binary.bin differ
diff --git a/tests/data/reference_files/small_text.txt b/tests/data/reference_files/small_text.txt
new file mode 100644
index 0000000..85d20ae
--- /dev/null
+++ b/tests/data/reference_files/small_text.txt
@@ -0,0 +1 @@
+Hello, World! This is a small text file.
diff --git a/tests/data/reference_files/subdirectory/subdir_file1.txt b/tests/data/reference_files/subdirectory/subdir_file1.txt
new file mode 100644
index 0000000..9b7eac5
--- /dev/null
+++ b/tests/data/reference_files/subdirectory/subdir_file1.txt
@@ -0,0 +1 @@
+File in subdirectory
diff --git a/tests/data/reference_files/subdirectory/subdir_file2.bin b/tests/data/reference_files/subdirectory/subdir_file2.bin
new file mode 100644
index 0000000..bd26b49
Binary files /dev/null and b/tests/data/reference_files/subdirectory/subdir_file2.bin differ
diff --git "a/tests/data/reference_files/unicode_name_\321\204\320\260\320\271\320\273.txt" "b/tests/data/reference_files/unicode_name_\321\204\320\260\320\271\320\273.txt"
new file mode 100644
index 0000000..b15958f
--- /dev/null
+++ "b/tests/data/reference_files/unicode_name_\321\204\320\260\320\271\320\273.txt"
@@ -0,0 +1 @@
+Файл с unicode именем
diff --git a/tests/data/reference_ntfs.img.gz b/tests/data/reference_ntfs.img.gz
new file mode 100644
index 0000000..7edc868
Binary files /dev/null and b/tests/data/reference_ntfs.img.gz differ
diff --git a/tests/data/reference_ntfs.json b/tests/data/reference_ntfs.json
new file mode 100644
index 0000000..7f765ec
--- /dev/null
+++ b/tests/data/reference_ntfs.json
@@ -0,0 +1,20 @@
+{
+ "created_by": "build_reference_ntfs.py",
+ "file_count": 9,
+ "image_hash": "864fcb2d2e0fc0ec2620e01929246fe263c68692c5793e90e24e180d844bebd7",
+ "image_size_bytes": 104857600,
+ "notes": "Reference NTFS filesystem for RecuperaBit E2E tests",
+ "reference_files_hashes": {
+ "deep/nested/directory/deep_file.txt": "c44594d1bf12bfe9b8f85ff08d44210cb0d7976ba292434b2c50e3cc8331e53c",
+ "empty_file.empty": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "file_with_ads.txt": "674a71cd25529aa999f590bed146fc496f85f2659aa7f1bdbadbef2ca36ea8a4",
+ "large_file.dat": "3031d04fa4ca07a7794b60b8eca58afc7cc7faea73b6d47352d526e906346d02",
+ "medium_binary.bin": "334bbc577b4b8075539ee42aca3a8a71b02861507294223087cdcdc9040e0f16",
+ "small_text.txt": "a1a5e8236ce8738b882d5cbd97af29414e75407db56d1c4668d261a6d9a17091",
+ "subdirectory/subdir_file1.txt": "33008bf127acca6e47249b311652515c5803249d25db5951da9f296297c6feae",
+ "subdirectory/subdir_file2.bin": "1ee7722ccd6ef215afa901f5a8bbeaf770952f7ccf2586ebf74e841e24b46d8f",
+ "unicode_name_\u0444\u0430\u0439\u043b.txt": "cebaf880b99881b574003c8220ac3013e2830b376d2d6b3f001db6e51f449d85"
+ },
+ "size_mb": 100,
+ "version": "1.0"
+}
\ No newline at end of file
diff --git a/tests/reference_image.py b/tests/reference_image.py
new file mode 100644
index 0000000..01d87a4
--- /dev/null
+++ b/tests/reference_image.py
@@ -0,0 +1,168 @@
+"""Reference NTFS image utilities for E2E tests."""
+
+import hashlib
+import json
+import logging
+import shutil
+from pathlib import Path
+from typing import Dict, Optional, Tuple
+
+import gzip
+
+
+class ReferenceNTFSImage:
+ """Handler for reference NTFS filesystem images used in E2E tests."""
+
+ def __init__(self, image_path: str = "tests/data/reference_ntfs.img.gz"):
+ self.image_path = Path(image_path)
+ # Remove both .img and .gz to get metadata path
+ if self.image_path.suffix == '.gz':
+ self.metadata_path = self.image_path.with_suffix('').with_suffix('.json')
+ else:
+ self.metadata_path = self.image_path.with_suffix('.json')
+ self.logger = logging.getLogger(__name__)
+
+ def exists(self) -> bool:
+ """Check if the reference image exists."""
+ print(self.image_path, self.metadata_path)
+ return self.image_path.exists() and self.metadata_path.exists()
+
+ def is_compressed(self) -> bool:
+ """Check if the reference image is compressed (e.g., .img.gz)."""
+ return self.image_path.suffix == '.gz'
+
+ def _compute_file_hash(self, filepath: Path, compressed: bool = False) -> str:
+ """Compute SHA256 hash of a file."""
+ sha256_hash = hashlib.sha256()
+ opener = gzip.open if compressed else open
+ with opener(filepath, "rb") as f:
+ for chunk in iter(lambda: f.read(4096), b""):
+ sha256_hash.update(chunk)
+ return sha256_hash.hexdigest()
+
+ def _compute_directory_hash(self, directory: Path) -> Dict[str, str]:
+ """Compute hashes for all files in a directory recursively."""
+ file_hashes = {}
+
+ for filepath in directory.rglob('*'):
+ if filepath.is_file():
+ relative_path = filepath.relative_to(directory)
+ file_hashes[str(relative_path)] = self._compute_file_hash(filepath)
+
+ return file_hashes
+
+ def validate(self) -> Tuple[bool, Optional[str]]:
+ """Validate that the reference image is up-to-date and uncorrupted.
+
+ Returns:
+ (is_valid, error_message)
+ """
+ if not self.exists():
+ return False, f"Reference image not found: {self.image_path}"
+
+ try:
+ # Load metadata
+ with open(self.metadata_path, 'r') as f:
+ metadata = json.load(f)
+
+ # Validate image hash
+ current_image_hash = self._compute_file_hash(self.image_path, self.is_compressed())
+ expected_image_hash = metadata.get('image_hash')
+
+ if current_image_hash != expected_image_hash:
+ return False, f"Image hash mismatch: expected {expected_image_hash}, got {current_image_hash}"
+
+ # Validate source files hash (to detect if reference files changed)
+ reference_files_dir = Path("tests/data/reference_files")
+ if reference_files_dir.exists():
+ current_files_hash = self._compute_directory_hash(reference_files_dir)
+ expected_files_hash = metadata.get('reference_files_hashes', {})
+
+ if current_files_hash != expected_files_hash:
+ return False, "Reference files have changed, image needs to be rebuilt"
+
+ return True, None
+
+ except Exception as e:
+ return False, f"Validation error: {e}"
+
+ def get_expected_files(self) -> Dict[str, str]:
+ """Get the expected file hashes from the reference image metadata.
+
+ Returns:
+ Dictionary mapping relative file paths to their SHA256 hashes
+ """
+ if not self.metadata_path.exists():
+ return {}
+
+ try:
+ with open(self.metadata_path, 'r') as f:
+ metadata = json.load(f)
+ return metadata.get('reference_files_hashes', {})
+ except Exception as e:
+ self.logger.error(f"Failed to load metadata: {e}")
+ return {}
+
+ def get_reference_files_dir(self) -> Path:
+ """Get the directory containing the original reference files."""
+ return self.metadata_path.parent / "reference_files"
+
+ def copy_to_temp(self, temp_path: Path) -> None:
+ """Copy the reference image to a temporary location for testing.
+
+ Args:
+ temp_path: Path where to copy the image
+ """
+ if not self.exists():
+ raise FileNotFoundError(f"Reference image not found: {self.image_path}")
+
+ if self.is_compressed():
+ with gzip.open(self.image_path, 'rb') as f_in, open(temp_path, 'wb') as f_out:
+ shutil.copyfileobj(f_in, f_out)
+ else:
+ shutil.copy2(self.image_path, temp_path)
+ self.logger.debug(f"Copied reference image to {temp_path}")
+
+ def get_info(self) -> Dict:
+ """Get information about the reference image.
+
+ Returns:
+ Dictionary with image metadata
+ """
+ if not self.metadata_path.exists():
+ return {}
+
+ try:
+ with open(self.metadata_path, 'r') as f:
+ return json.load(f)
+ except Exception as e:
+ self.logger.error(f"Failed to load metadata: {e}")
+ return {}
+
+
+def ensure_reference_image() -> ReferenceNTFSImage:
+ """Ensure reference NTFS image exists and is valid.
+
+ Returns:
+ ReferenceNTFSImage instance
+
+ Raises:
+ FileNotFoundError: If image doesn't exist
+ ValueError: If image is corrupted or outdated
+ """
+ ref_image = ReferenceNTFSImage()
+
+ if not ref_image.exists():
+ raise FileNotFoundError(
+ "Reference NTFS image not found. Please run: "
+ "sudo python tools/build_reference_ntfs.py"
+ )
+
+ is_valid, error = ref_image.validate()
+ if not is_valid:
+ raise ValueError(
+ f"Reference NTFS image validation failed: {error}. "
+ "Please rebuild with: sudo python tools/build_reference_ntfs.py"
+ )
+
+ return ref_image
diff --git a/tests/run_tests.py b/tests/run_tests.py
new file mode 100644
index 0000000..84a523b
--- /dev/null
+++ b/tests/run_tests.py
@@ -0,0 +1,125 @@
+"""Test runner and configuration for RecuperaBit test suite."""
+
+import unittest
+import sys
+import os
+import logging
+from pathlib import Path
+
+# Add the project root to the Python path
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+# Import test modules
+from tests.test_ntfs_unit import *
+from tests.test_ntfs_e2e import *
+from tests.test_integration import *
+
+
+def create_test_suite():
+ """Create and return the complete test suite."""
+ loader = unittest.TestLoader()
+ suite = unittest.TestSuite()
+
+ # Add unit tests
+ suite.addTests(loader.loadTestsFromModule(sys.modules['tests.test_ntfs_unit']))
+
+ # Add integration tests
+ suite.addTests(loader.loadTestsFromModule(sys.modules['tests.test_integration']))
+
+ # Add E2E tests (these may be skipped if tools are not available)
+ suite.addTests(loader.loadTestsFromModule(sys.modules['tests.test_ntfs_e2e']))
+
+ return suite
+
+
+def run_unit_tests_only():
+ """Run only unit tests (fast, no external dependencies)."""
+ loader = unittest.TestLoader()
+ suite = unittest.TestSuite()
+
+ suite.addTests(loader.loadTestsFromModule(sys.modules['tests.test_ntfs_unit']))
+ suite.addTests(loader.loadTestsFromModule(sys.modules['tests.test_integration']))
+
+ runner = unittest.TextTestRunner(verbosity=2)
+ result = runner.run(suite)
+ return result.wasSuccessful()
+
+
+def run_e2e_tests_only():
+ """Run only end-to-end tests (slower, requires system tools)."""
+ loader = unittest.TestLoader()
+ suite = unittest.TestSuite()
+
+ suite.addTests(loader.loadTestsFromModule(sys.modules['tests.test_ntfs_e2e']))
+
+ runner = unittest.TextTestRunner(verbosity=2)
+ result = runner.run(suite)
+ return result.wasSuccessful()
+
+
+def run_all_tests():
+ """Run the complete test suite."""
+ suite = create_test_suite()
+ runner = unittest.TextTestRunner(verbosity=2)
+ result = runner.run(suite)
+ return result.wasSuccessful()
+
+
+def main():
+ """Main test runner with command line options."""
+ import argparse
+
+ parser = argparse.ArgumentParser(description='RecuperaBit Test Runner')
+ parser.add_argument('--unit', action='store_true',
+ help='Run only unit tests (fast)')
+ parser.add_argument('--e2e', action='store_true',
+ help='Run only end-to-end tests (requires system tools)')
+ parser.add_argument('--integration', action='store_true',
+ help='Run only integration tests')
+ parser.add_argument('--verbose', '-v', action='store_true',
+ help='Verbose logging output')
+ parser.add_argument('--debug', action='store_true',
+ help='Debug level logging')
+
+ args = parser.parse_args()
+
+ # Set up logging
+ log_level = logging.WARNING
+ if args.verbose:
+ log_level = logging.INFO
+ if args.debug:
+ log_level = logging.DEBUG
+
+ logging.basicConfig(
+ level=log_level,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ )
+
+ # Run selected tests
+ success = True
+
+ if args.unit:
+ print("Running unit tests only...")
+ success = run_unit_tests_only()
+ elif args.e2e:
+ print("Running end-to-end tests only...")
+ success = run_e2e_tests_only()
+ elif args.integration:
+ print("Running integration tests only...")
+ loader = unittest.TestLoader()
+ suite = unittest.TestSuite()
+ suite.addTests(loader.loadTestsFromModule(sys.modules['tests.test_integration']))
+ runner = unittest.TextTestRunner(verbosity=2)
+ result = runner.run(suite)
+ success = result.wasSuccessful()
+ else:
+ print("Running complete test suite...")
+ success = run_all_tests()
+
+ # Exit with appropriate code
+ sys.exit(0 if success else 1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/test_integration.py b/tests/test_integration.py
new file mode 100644
index 0000000..3e3e806
--- /dev/null
+++ b/tests/test_integration.py
@@ -0,0 +1,347 @@
+"""Integration tests for RecuperaBit logic and utilities."""
+
+import unittest
+import tempfile
+import os
+from unittest.mock import Mock, patch
+from io import BytesIO
+
+from recuperabit.logic import SparseList, approximate_matching
+from recuperabit.utils import merge, sectors, unpack
+
+
+class TestSparseListIntegration(unittest.TestCase):
+ """Integration tests for SparseList with NTFS components."""
+
+ def test_sparse_list_with_mft_references(self):
+ """Test SparseList with MFT-like reference patterns."""
+ # Simulate MFT record references
+ mft_refs = {
+ 0: 0, # Root directory points to itself
+ 16: 0, # System file points to root
+ 32: 16, # File in system directory
+ 48: 0, # Another root-level file
+ 64: 48, # File in subdirectory
+ 80: 48, # Another file in same subdirectory
+ }
+
+ sparse_list = SparseList(mft_refs)
+
+ # Test basic operations
+ self.assertEqual(sparse_list[0], 0)
+ self.assertEqual(sparse_list[16], 0)
+ self.assertEqual(sparse_list[64], 48)
+ self.assertIsNone(sparse_list[24]) # Gap
+
+ # Test length
+ self.assertEqual(len(sparse_list), 81)
+
+ # Test iteration gives keys, not all indices
+ keys = list(sparse_list)
+ expected_keys = [0, 16, 32, 48, 64, 80]
+ self.assertEqual(keys, expected_keys)
+
+ def test_sparse_list_large_gaps(self):
+ """Test SparseList with large gaps (common in fragmented filesystems)."""
+ fragmented_refs = {
+ 100: 0,
+ 5000: 100,
+ 10000: 5000,
+ 50000: 10000,
+ }
+
+ sparse_list = SparseList(fragmented_refs)
+
+ # Should handle large indices efficiently
+ self.assertEqual(sparse_list[100], 0)
+ self.assertEqual(sparse_list[5000], 100)
+ self.assertEqual(sparse_list[50000], 10000)
+
+ # Large gaps should return None
+ self.assertIsNone(sparse_list[1000])
+ self.assertIsNone(sparse_list[25000])
+
+
+class TestApproximateMatching(unittest.TestCase):
+ """Test approximate matching functionality."""
+
+ def test_approximate_matching_perfect_match(self):
+ """Test approximate matching with perfect match."""
+ # Create text (haystack) and pattern (needle)
+ text_data = {i: i // 4 for i in range(0, 100, 4)} # Every 4th position
+ pattern_data = {i: i // 4 for i in range(0, 20, 4)} # First 5 elements
+
+ text_list = SparseList(text_data)
+ pattern_list = SparseList(pattern_data)
+
+ # Should find match at position 0
+ result = approximate_matching(text_list, pattern_list, 0, k=3)
+
+ self.assertIsNotNone(result)
+ positions, count, percentage = result
+ self.assertIn(0, positions)
+ self.assertGreater(percentage, 0.8) # High match percentage
+
+ def test_approximate_matching_shifted_pattern(self):
+ """Test approximate matching with shifted pattern."""
+ # Create text and pattern with some overlap
+ text_data = {i: i % 5 for i in range(0, 100, 4)} # Pattern repeating every 5
+ pattern_data = {i: i % 5 for i in range(0, 20, 4)} # Same pattern but shorter
+
+ text_list = SparseList(text_data)
+ pattern_list = SparseList(pattern_data)
+
+ # Should find matches at multiple positions
+ result = approximate_matching(text_list, pattern_list, 50, k=1)
+
+ if result is not None:
+ positions, count, percentage = result
+ # positions is a set, not a list, and contains actual match positions
+ self.assertIsInstance(positions, set)
+ self.assertGreater(len(positions), 0)
+ else:
+ # If no exact match found, that's also acceptable for this pattern
+ self.assertIsNone(result)
+
+ def test_approximate_matching_no_match(self):
+ """Test approximate matching with no good match."""
+ # Create text and completely different pattern
+ text_data = {i: 1 for i in range(0, 100, 4)} # All 1s
+ pattern_data = {i: 2 for i in range(0, 20, 4)} # All 2s
+
+ text_list = SparseList(text_data)
+ pattern_list = SparseList(pattern_data)
+
+ # Should not find good match
+ result = approximate_matching(text_list, pattern_list, 0, k=3)
+
+ if result is not None:
+ positions, count, percentage = result
+ self.assertLess(percentage, 0.1) # Very low match percentage
+
+
+class TestUtilityFunctions(unittest.TestCase):
+ """Test utility functions."""
+
+ def test_merge_function(self):
+ """Test the merge function."""
+ from recuperabit.fs.core_types import Partition, File
+
+ # Create mock scanner
+ class MockScanner:
+ pass
+ scanner = MockScanner()
+
+ # Create partition objects with files
+ part1 = Partition('TEST', 0, scanner)
+ part2 = Partition('TEST', 0, scanner)
+
+ # Add files to partitions
+ file1 = File(1, 'file1.txt', 100)
+ file2 = File(2, 'file2.txt', 200)
+ file3 = File(3, 'file3.txt', 300)
+ file4 = File(4, 'file4.txt', 400)
+
+ part1.add_file(file1)
+ part1.add_file(file2)
+ part2.add_file(file3)
+ part2.add_file(file4)
+
+ # Test merge
+ merge(part1, part2)
+
+ # part1 should now contain files from both
+ self.assertIn(1, part1.files)
+ self.assertIn(2, part1.files)
+ self.assertIn(3, part1.files)
+ self.assertIn(4, part1.files)
+ self.assertEqual(len(part1.files), 4)
+
+ def test_merge_with_conflicts(self):
+ """Test merge function with conflicting keys."""
+ from recuperabit.fs.core_types import Partition, File
+
+ # Create mock scanner
+ class MockScanner:
+ pass
+ scanner = MockScanner()
+
+ # Create partition objects
+ part1 = Partition('TEST', 0, scanner)
+ part2 = Partition('TEST', 0, scanner)
+
+ # Add conflicting files (same index)
+ file1_ghost = File(1, 'file1_ghost.txt', 100, is_ghost=True)
+ file1_real = File(1, 'file1_real.txt', 100, is_ghost=False)
+ file2 = File(2, 'file2.txt', 200)
+ file3 = File(3, 'file3.txt', 300)
+
+ part1.add_file(file1_ghost)
+ part1.add_file(file2)
+ part2.add_file(file1_real)
+ part2.add_file(file3)
+
+ merge(part1, part2)
+
+ # part1 should replace ghost with real file
+ self.assertIn(1, part1.files)
+ self.assertIn(2, part1.files)
+ self.assertIn(3, part1.files)
+ # The ghost file should be replaced by the real file
+ self.assertFalse(part1.files[1].is_ghost)
+
+ def test_sectors_function(self):
+ """Test the sectors function."""
+ # Create test data
+ test_data = b'A' * 512 + b'B' * 512 + b'C' * 512 # 3 sectors
+ test_file = BytesIO(test_data)
+
+ # Test reading single sector
+ result = sectors(test_file, 0, 1)
+ self.assertEqual(result, b'A' * 512)
+
+ # Test reading multiple sectors
+ result = sectors(test_file, 1, 2)
+ self.assertEqual(result, b'B' * 512 + b'C' * 512)
+
+ # Test reading with byte granularity
+ result = sectors(test_file, 256, 512, 1) # 512 bytes starting at byte 256
+ expected = b'A' * 256 + b'B' * 256
+ self.assertEqual(result, expected)
+
+ def test_sectors_out_of_bounds(self):
+ """Test sectors function with out-of-bounds access."""
+ test_data = b'A' * 512 # Only 1 sector
+ test_file = BytesIO(test_data)
+
+ # Try to read beyond file
+ result = sectors(test_file, 1, 1)
+ self.assertEqual(result, b'') # Should return empty bytes
+
+ def test_unpack_function(self):
+ """Test the unpack function with simple format."""
+ # Create test data
+ test_data = b'\x01\x02\x03\x04\x05\x06\x07\x08'
+
+ # Create format specification: [(label, (formatter, lower, higher)), ...]
+ test_format = [
+ ('first_byte', ('i', 0, 0)), # Single byte at position 0
+ ('two_bytes', ('2i', 1, 2)), # Two bytes from position 1-2
+ ('last_four', ('4i', 4, 7)) # Four bytes from position 4-7
+ ]
+
+ result = unpack(test_data, test_format)
+
+ # Check that we get expected structure
+ self.assertIn('first_byte', result)
+ self.assertIn('two_bytes', result)
+ self.assertIn('last_four', result)
+
+ def test_unpack_insufficient_data(self):
+ """Test unpack function with insufficient data."""
+ # Create short test data
+ test_data = b'\x01\x02'
+
+ # Format that requires more data than available
+ test_format = [
+ ('valid_data', ('i', 0, 1)), # Valid range
+ ('out_of_bounds', ('i', 5, 8)) # Tries to read beyond data
+ ]
+
+ # Should handle gracefully, setting None for missing data
+ result = unpack(test_data, test_format)
+
+ # Should have valid data for first field
+ self.assertIn('valid_data', result)
+ # Should handle out of bounds gracefully
+ self.assertIn('out_of_bounds', result)
+
+ def test_unpack_insufficient_data(self):
+ """Test unpack function with insufficient data."""
+ # Create short test data
+ test_data = b'\x01\x02'
+
+ # Format that requires more data than available
+ test_format = [
+ ('valid_data', ('i', 0, 1)), # Valid range
+ ('out_of_bounds', ('i', 5, 8)) # Tries to read beyond data
+ ]
+
+ # Should handle gracefully, setting None for missing data
+ result = unpack(test_data, test_format)
+
+ # Should have valid data for first field
+ self.assertIn('valid_data', result)
+ # Should handle out of bounds gracefully
+ self.assertIn('out_of_bounds', result)
+
+
+class TestNTFSIntegration(unittest.TestCase):
+ """Integration tests combining multiple NTFS components."""
+
+ def test_mft_indx_relationship(self):
+ """Test the relationship between MFT and INDX records."""
+ # Simulate finding related MFT and INDX records
+ mft_positions = {100, 200, 300, 400} # MFT record positions
+ indx_positions = {1000, 2000, 3000} # INDX record positions
+
+ # Simulate INDX records pointing to MFT records
+ indx_references = {
+ 1000: {'parent': 100, 'children': {200, 300}},
+ 2000: {'parent': 200, 'children': {400}},
+ 3000: {'parent': 300, 'children': set()},
+ }
+
+ # Create SparseList for INDX relationships
+ indx_list = SparseList({pos: info['parent'] for pos, info in indx_references.items()})
+
+ # Verify relationships
+ self.assertEqual(indx_list[1000], 100) # INDX at 1000 points to MFT 100
+ self.assertEqual(indx_list[2000], 200) # INDX at 2000 points to MFT 200
+
+ # Test that we can find directory structure
+ root_mft = 100
+ subdirs = [pos for pos, info in indx_references.items() if info['parent'] == root_mft]
+ self.assertEqual(len(subdirs), 1)
+ self.assertEqual(subdirs[0], 1000)
+
+ # Test children relationships
+ children_of_200 = indx_references[2000]['children']
+ self.assertEqual(children_of_200, {400})
+
+ def test_partition_boundary_detection(self):
+ """Test partition boundary detection logic."""
+ # Simulate MFT pattern for boundary detection
+ base_pattern = {10: 100, 20: 100, 30: 200, 40: 200} # Cluster -> MFT record
+
+ # Test different sectors per cluster values
+ for sec_per_clus in [1, 2, 4, 8]:
+ # Convert cluster pattern to sector pattern
+ sector_pattern = {
+ cluster * sec_per_clus: mft_record
+ for cluster, mft_record in base_pattern.items()
+ }
+
+ pattern_list = SparseList(sector_pattern)
+
+ # Simulate text list (found INDX records)
+ text_data = {}
+ for sector in range(0, 400):
+ if sector in sector_pattern:
+ text_data[sector + 1000] = sector_pattern[sector] # Offset by 1000
+
+ text_list = SparseList(text_data)
+
+ # Test approximate matching for boundary detection
+ mft_address = 1000 # Assumed MFT start
+ result = approximate_matching(text_list, pattern_list, mft_address + min(sector_pattern.keys()), k=2)
+
+ if result is not None:
+ positions, count, percentage = result
+ # Should find at least one potential boundary
+ self.assertGreater(len(positions), 0)
+ self.assertGreater(percentage, 0.1)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_ntfs_e2e.py b/tests/test_ntfs_e2e.py
new file mode 100644
index 0000000..ad0bdc6
--- /dev/null
+++ b/tests/test_ntfs_e2e.py
@@ -0,0 +1,251 @@
+"""End-to-end tests for RecuperaBit NTFS recovery.
+
+This module uses pre-built reference NTFS images to test complete recovery workflows.
+"""
+
+import unittest
+import tempfile
+import os
+import shutil
+import hashlib
+from pathlib import Path
+from typing import Dict
+import logging
+
+from recuperabit.fs.ntfs import NTFSPartition, NTFSScanner
+from tests.reference_image import ensure_reference_image
+import main # Import main module to access interpret function
+
+
+class TestNTFSE2E(unittest.TestCase):
+ """End-to-end tests for NTFS recovery."""
+
+ @classmethod
+ def setUpClass(cls):
+ """Set up class-level fixtures."""
+ # Ensure reference image exists and is valid
+ try:
+ cls.ref_image = ensure_reference_image()
+ logging.info(f"Using reference NTFS image: {cls.ref_image.image_path}")
+ except (FileNotFoundError, ValueError) as e:
+ raise unittest.SkipTest(f"Reference image not available: {e}")
+
+ # Get expected file hashes from reference image
+ cls.expected_files = cls.ref_image.get_expected_files()
+
+ if not cls.expected_files:
+ raise unittest.SkipTest("No expected files in reference image metadata")
+
+ logging.info(f"Reference image contains {len(cls.expected_files)} test files")
+
+ # Set up temp directory for working files
+ cls.test_dir = tempfile.mkdtemp(prefix='recuperabit_e2e_')
+ cls.recovery_dir = os.path.join(cls.test_dir, 'recovered')
+ os.makedirs(cls.recovery_dir, exist_ok=True)
+
+ logging.basicConfig(level=logging.DEBUG)
+
+ @classmethod
+ def tearDownClass(cls):
+ """Clean up class-level fixtures."""
+ # Clean up test directory
+ if hasattr(cls, 'test_dir') and os.path.exists(cls.test_dir):
+ shutil.rmtree(cls.test_dir)
+
+ def setUp(self):
+ """Set up test fixtures."""
+ # Create a temporary copy of the reference image for this test
+ self.image_path = os.path.join(self.test_dir, f'test_ntfs_{id(self)}.img')
+ self.ref_image.copy_to_temp(Path(self.image_path))
+
+ def tearDown(self):
+ """Clean up test fixtures."""
+ # Clean up the temporary image copy
+ if hasattr(self, 'image_path') and os.path.exists(self.image_path):
+ os.remove(self.image_path)
+
+ def _scan_image_with_scanner(self, scanner_class: type[NTFSScanner]) -> Dict[int, NTFSPartition]:
+ """Scan the image with the given scanner class."""
+ # Keep file handle open and return it along with partitions
+ img_file = open(self.image_path, 'rb')
+ scanner = scanner_class(img_file)
+
+ # Feed sectors to scanner
+ sector_size = 512
+ sector_index = 0
+
+ while True:
+ img_file.seek(sector_index * sector_size)
+ sector = img_file.read(sector_size)
+
+ if len(sector) < sector_size:
+ break
+
+ result = scanner.feed(sector_index, sector)
+ if result:
+ logging.debug(f"Found {result} at sector {sector_index}")
+
+ sector_index += 1
+
+ # Get partitions
+ partitions = scanner.get_partitions()
+ # Store the file handle so it doesn't get closed
+ self._img_file = img_file
+ return partitions
+
+ def _close_image_file(self):
+ """Close the image file handle."""
+ if hasattr(self, '_img_file') and self._img_file:
+ self._img_file.close()
+ self._img_file = None
+
+ def _recover_files_from_partition(self, partition: NTFSPartition, partition_id: int) -> Dict[str, bytes]:
+ """Recover files from a partition using high-level interpret function with proper hierarchy."""
+ # Create temporary recovery directory
+ recovery_dir = os.path.join(self.test_dir, f'recovered_partition_{partition_id}')
+ os.makedirs(recovery_dir, exist_ok=True)
+
+ # Create shorthands structure like main.py
+ parts = {0: partition} # Simple mapping for our single partition
+ shorthands = [(0, 0)] # (index, partition_key) pairs
+
+ try:
+ # Use the high-level interpret function to restore the root directory
+ # This will properly handle filesystem hierarchy, directories, etc.
+ main.interpret('restore', ['0', '5'], parts, shorthands, recovery_dir) # '5' is typically the root directory
+
+ # Collect all recovered files and their content
+ recovered_files = {}
+
+ # Walk through the recovered directory structure
+ partition_dir = os.path.join(recovery_dir, 'Partition0', 'Root')
+ if os.path.exists(partition_dir):
+ for root, dirs, files in os.walk(partition_dir):
+ for file in files:
+ file_path = os.path.join(root, file)
+ # Get relative path from partition directory
+ relative_path = os.path.relpath(file_path, partition_dir)
+
+ try:
+ with open(file_path, 'rb') as f:
+ content = f.read()
+ recovered_files[relative_path] = content
+ logging.info(f"Recovered file: {relative_path} ({len(content)} bytes)")
+ except Exception as e:
+ logging.error(f"Error reading recovered file {relative_path}: {e}")
+
+ return recovered_files
+
+ except Exception as e:
+ logging.error(f"Error during recovery: {e}")
+ return {}
+ finally:
+ # Clean up recovery directory
+ if os.path.exists(recovery_dir):
+ shutil.rmtree(recovery_dir, ignore_errors=True)
+
+ def _compare_files(self, original_hashes: Dict[str, str],
+ recovered_files: Dict[str, bytes]) -> Dict[str, bool]:
+ """Compare original and recovered files, handling path normalization."""
+ results = {}
+
+ # Normalize recovered file paths by removing Root/ prefix
+
+ print(f"DEBUG: Expected files: {list(original_hashes.keys())}")
+ print(f"DEBUG: Normalized recovered files: {list(recovered_files.keys())}")
+
+ expected_recovered_files = [filename for filename in list(recovered_files.keys()) if filename in original_hashes.keys()]
+ print(f"DEBUG: Matching recovered files: {expected_recovered_files} ({len(expected_recovered_files)} / {len(original_hashes)})")
+
+ # Check how many files were recovered successfully
+ for filename, original_hash in original_hashes.items():
+ if filename in recovered_files:
+ recovered_file = recovered_files[filename]
+ recovered_hash = hashlib.sha256(recovered_file).hexdigest()
+ results[filename] = (original_hash == recovered_hash)
+ if results[filename]:
+ logging.info(f"✓ {filename}: Recovery successful ({len(recovered_file)} bytes)")
+ else:
+ logging.error(f"✗ {filename}: Hash mismatch! Expected: {original_hash}, Got: {recovered_hash}")
+ # Print first 64 bytes of recovered content vs the original content for debugging
+ logging.error(f" Recovered content (first 64 bytes): {recovered_file[:64]}")
+ with open(self.ref_image.get_reference_files_dir() / filename, 'rb') as original_file:
+ original_content = original_file.read(64)
+ logging.error(f" Original content (first 64 bytes): {original_content[:64]}")
+ else:
+ results[filename] = False
+ logging.error(f"✗ {filename}: File not recovered")
+
+ return results
+
+ def test_basic_ntfs_recovery(self):
+ """Test basic NTFS file recovery using reference image."""
+ print(f"DEBUG: Using reference NTFS image at {self.image_path}")
+
+ try:
+ # Test recovery with standard scanner
+ partitions = self._scan_image_with_scanner(NTFSScanner)
+ self.assertGreater(len(partitions), 0, "No NTFS partitions found")
+
+ # Recover files from the LARGEST partition (most likely to contain user data)
+ if not partitions:
+ self.fail("No NTFS partitions found")
+
+ # Find the largest partition by number of files (user data indicator)
+ largest_partition_id = None
+ largest_partition = None
+ max_files = 0
+
+ print(f"DEBUG: Found {len(partitions)} partitions:")
+ for partition_id, partition in partitions.items():
+ file_count = len(partition.files) if hasattr(partition, 'files') else 0
+ print(f" Partition {partition_id}: {file_count} files, offset {partition.offset}")
+
+ if file_count > max_files:
+ max_files = file_count
+ largest_partition_id = partition_id
+ largest_partition = partition
+
+ if largest_partition is None:
+ self.fail("No partition with files found")
+
+ print(f"DEBUG: Processing largest partition {largest_partition_id} with {max_files} files at offset {largest_partition.offset}")
+
+ # Recover files from the largest partition only
+ all_recovered_files = self._recover_files_from_partition(largest_partition, largest_partition_id)
+
+ for filename, content in all_recovered_files.items():
+ print(f"DEBUG: Recovered file '{filename}' with content size {len(content)} bytes")
+
+ # Compare results using expected files from reference image
+ comparison = self._compare_files(self.expected_files, all_recovered_files)
+
+ # Check that at least some files were recovered correctly
+ successful_recoveries = sum(1 for success in comparison.values() if success)
+ total_files = len(self.expected_files)
+
+ self.assertGreater(successful_recoveries, 0, "No files recovered successfully")
+
+ # We expect most files to be recovered (allowing for some edge cases)
+ recovery_rate = successful_recoveries / total_files
+ self.assertAlmostEqual(recovery_rate, 1.0,
+ f"Low recovery rate: {recovery_rate:.2%} ({successful_recoveries}/{total_files})")
+
+ # Log success for visibility
+ print(f"SUCCESS: Hierarchical recovery rate {recovery_rate:.2%} ({successful_recoveries}/{total_files})")
+ print(f"✅ All {total_files} files found with correct filesystem hierarchy!")
+ print(f"✅ High-level recovery APIs working correctly!")
+ print(f"✅ Largest partition selection working!")
+ finally:
+ # Always close the image file handle
+ self._close_image_file()
+
+
+if __name__ == '__main__':
+ # Set up logging
+ logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s'
+ )
+
+ unittest.main(verbosity=2)
diff --git a/tests/test_ntfs_unit.py b/tests/test_ntfs_unit.py
new file mode 100644
index 0000000..a35d24e
--- /dev/null
+++ b/tests/test_ntfs_unit.py
@@ -0,0 +1,297 @@
+"""Unit tests for NTFS parsing functions and core types."""
+
+import unittest
+from unittest.mock import Mock
+
+# Import the modules under test
+from recuperabit.fs.ntfs import (
+ NTFSFile, NTFSPartition,
+ NTFSScanner, best_name, _apply_fixup_values
+)
+from recuperabit.logic import SparseList
+
+
+class TestNTFSParsing(unittest.TestCase):
+ """Test NTFS parsing functions."""
+
+ def setUp(self):
+ """Set up test fixtures."""
+ # Create a mock MFT entry for testing
+ self.mock_mft_entry = bytearray(1024) # 1KB MFT entry
+ # FILE signature
+ self.mock_mft_entry[0:4] = b'FILE'
+ # Fixup offset at position 4-6 (little endian)
+ self.mock_mft_entry[4:6] = (48).to_bytes(2, 'little')
+ # Number of fixup entries at position 6-8
+ self.mock_mft_entry[6:8] = (2).to_bytes(2, 'little')
+ # First attribute offset at position 20-22
+ self.mock_mft_entry[20:22] = (56).to_bytes(2, 'little')
+ # MFT record size allocated at position 28-32
+ self.mock_mft_entry[28:32] = (1024).to_bytes(4, 'little')
+ # Record number at position 44-48
+ self.mock_mft_entry[44:48] = (42).to_bytes(4, 'little')
+
+ # Mock INDX entry
+ self.mock_indx_entry = bytearray(4096) # 4KB INDX entry
+ # INDX signature
+ self.mock_indx_entry[0:4] = b'INDX'
+ # Fixup offset at position 4-6
+ self.mock_indx_entry[4:6] = (40).to_bytes(2, 'little')
+ # Number of fixup entries at position 6-8
+ self.mock_indx_entry[6:8] = (8).to_bytes(2, 'little')
+
+ def test_apply_fixup_values(self):
+ """Test the fixup values application."""
+ # Create a test entry with 3 sectors (1536 bytes) to test both fixups
+ entry = bytearray(1536)
+ header = {
+ 'off_fixup': 48,
+ 'n_entries': 3 # 1 original + 2 fixup entries
+ }
+
+ # Set up fixup array at offset 48
+ entry[48:50] = b'\xAA\xBB' # Original value (not used in replacement)
+ entry[50:52] = b'\xCC\xDD' # First replacement (for sector 1)
+ entry[52:54] = b'\xEE\xFF' # Second replacement (for sector 2)
+
+ # Set sectors to have the original values that need fixing
+ # sector_size = 512, so positions are 512*i - 2
+ entry[510:512] = b'\x00\x00' # End of first sector (512*1 - 2)
+ entry[1022:1024] = b'\x00\x00' # End of second sector (512*2 - 2)
+
+ _apply_fixup_values(header, entry)
+
+ # Check that fixup was applied correctly
+ self.assertEqual(entry[510:512], b'\xCC\xDD')
+ self.assertEqual(entry[1022:1024], b'\xEE\xFF')
+
+ def test_best_name(self):
+ """Test the best_name function."""
+ # Test with NTFS namespace (preferred)
+ entries = [(1, 'short.txt'), (3, 'long_filename.txt')]
+ self.assertEqual(best_name(entries), 'long_filename.txt')
+
+ # Test without NTFS namespace
+ entries = [(1, 'short.txt'), (2, 'dos_name.txt')]
+ self.assertEqual(best_name(entries), 'short.txt')
+
+ # Test with empty list
+ self.assertIsNone(best_name([]))
+
+ # Test with empty name
+ entries = [(3, '')]
+ self.assertIsNone(best_name(entries))
+
+
+class TestNTFSFile(unittest.TestCase):
+ """Test NTFSFile class."""
+
+ def setUp(self):
+ """Set up test fixtures."""
+ self.mock_parsed = {
+ 'record_n': 42,
+ 'flags': 0x01, # Not deleted
+ 'attributes': {
+ '$FILE_NAME': [{
+ 'content': {
+ 'namespace': 3,
+ 'name': 'test_file.txt',
+ 'name_length': 13,
+ 'parent_entry': 5
+ }
+ }],
+ '$DATA': [{
+ 'name': '',
+ 'real_size': 1024,
+ 'non_resident': False,
+ 'content_size': 1024
+ }],
+ '$STANDARD_INFORMATION': {
+ 'content': {
+ 'modification_time': 132000000000000000,
+ 'access_time': 132000000000000000,
+ 'creation_time': 132000000000000000
+ }
+ }
+ }
+ }
+
+ def test_ntfs_file_creation(self):
+ """Test NTFSFile creation with valid data."""
+ file_obj = NTFSFile(self.mock_parsed, 12345)
+
+ self.assertEqual(file_obj.index, 42)
+ self.assertEqual(file_obj.name, 'test_file.txt')
+ self.assertEqual(file_obj.size, 1024)
+ self.assertFalse(file_obj.is_directory)
+ self.assertFalse(file_obj.is_deleted)
+ self.assertFalse(file_obj.is_ghost)
+ self.assertEqual(file_obj.parent, 5)
+ self.assertEqual(file_obj.ads, '')
+
+ def test_ntfs_file_with_ads(self):
+ """Test NTFSFile creation with alternate data stream."""
+ file_obj = NTFSFile(self.mock_parsed, 12345, ads='stream1')
+
+ self.assertEqual(file_obj.index, '42:stream1')
+ self.assertEqual(file_obj.name, 'test_file.txt:stream1')
+ self.assertEqual(file_obj.ads, 'stream1')
+
+ def test_ntfs_file_directory(self):
+ """Test NTFSFile creation for directory."""
+ self.mock_parsed['flags'] = 0x03 # Directory flag
+ file_obj = NTFSFile(self.mock_parsed, 12345)
+
+ self.assertTrue(file_obj.is_directory)
+
+ def test_ntfs_file_deleted(self):
+ """Test NTFSFile creation for deleted file."""
+ self.mock_parsed['flags'] = 0x00 # Deleted flag
+ file_obj = NTFSFile(self.mock_parsed, 12345)
+
+ self.assertTrue(file_obj.is_deleted)
+
+ def test_ntfs_file_ghost(self):
+ """Test NTFSFile creation for ghost file."""
+ file_obj = NTFSFile(self.mock_parsed, 12345, is_ghost=True)
+
+ self.assertTrue(file_obj.is_ghost)
+
+ def test_ntfs_file_ignore(self):
+ """Test NTFSFile ignore logic."""
+ # Test $Bad file
+ self.mock_parsed['record_n'] = 8
+ file_obj = NTFSFile(self.mock_parsed, 12345, ads='$Bad')
+ file_obj.index = '8:$Bad'
+ self.assertTrue(file_obj.ignore())
+
+ # Test $UsnJrnl file
+ self.mock_parsed['record_n'] = 100
+ file_obj = NTFSFile(self.mock_parsed, 12345, ads='$J')
+ file_obj.parent = 11
+ self.assertTrue(file_obj.ignore())
+
+
+class TestNTFSPartition(unittest.TestCase):
+ """Test NTFSPartition class."""
+
+ def setUp(self):
+ """Set up test fixtures."""
+ self.scanner = Mock(spec=NTFSScanner)
+
+ def test_ntfs_partition_creation(self):
+ """Test NTFSPartition creation."""
+ partition = NTFSPartition(self.scanner, 12345)
+
+ self.assertEqual(partition.fs_type, 'NTFS')
+ self.assertEqual(partition.root_id, 5)
+ self.assertEqual(partition.mft_pos, 12345)
+ self.assertIsNone(partition.sec_per_clus)
+ self.assertIsNone(partition.mftmirr_pos)
+
+ def test_ntfs_partition_additional_repr(self):
+ """Test NTFSPartition additional representation."""
+ partition = NTFSPartition(self.scanner, 12345)
+ partition.sec_per_clus = 8
+ partition.mftmirr_pos = 67890
+
+ additional = partition.additional_repr()
+ expected = [
+ ('Sec/Clus', 8),
+ ('MFT offset', 12345),
+ ('MFT mirror offset', 67890)
+ ]
+ self.assertEqual(additional, expected)
+
+
+class TestNTFSScanner(unittest.TestCase):
+ """Test NTFSScanner class."""
+
+ def setUp(self):
+ """Set up test fixtures."""
+ self.scanner = NTFSScanner(Mock())
+
+ def test_feed_boot_sector(self):
+ """Test feeding a boot sector."""
+ boot_sector = b'NTFS' + b'\x00' * 506 + b'\x55\xAA'
+ result = self.scanner.feed(0, boot_sector)
+
+ self.assertEqual(result, 'NTFS boot sector')
+ self.assertIn(0, self.scanner.found_boot)
+
+ def test_feed_file_record(self):
+ """Test feeding a FILE record."""
+ file_record = b'FILE' + b'\x00' * 508
+ result = self.scanner.feed(100, file_record)
+
+ self.assertEqual(result, 'NTFS file record')
+ self.assertIn(100, self.scanner.found_file)
+
+ def test_feed_baad_record(self):
+ """Test feeding a BAAD record."""
+ baad_record = b'BAAD' + b'\x00' * 508
+ result = self.scanner.feed(200, baad_record)
+
+ self.assertEqual(result, 'NTFS file record')
+ self.assertIn(200, self.scanner.found_file)
+
+ def test_feed_indx_record(self):
+ """Test feeding an INDX record."""
+ indx_record = b'INDX' + b'\x00' * 508
+ result = self.scanner.feed(300, indx_record)
+
+ self.assertEqual(result, 'NTFS index record')
+ self.assertIn(300, self.scanner.found_indx)
+
+ def test_feed_unknown_sector(self):
+ """Test feeding an unknown sector."""
+ unknown_sector = b'UNKN' + b'\x00' * 508
+ result = self.scanner.feed(400, unknown_sector)
+
+ self.assertIsNone(result)
+ self.assertNotIn(400, self.scanner.found_boot)
+ self.assertNotIn(400, self.scanner.found_file)
+ self.assertNotIn(400, self.scanner.found_indx)
+
+ def test_most_likely_sec_per_clus(self):
+ """Test most_likely_sec_per_clus function."""
+ self.scanner.found_spc = [8, 8, 8, 4, 4, 16]
+ result = self.scanner.most_likely_sec_per_clus()
+
+ # Should return 8 first (most common), then others
+ self.assertEqual(result[0], 8)
+ self.assertIn(4, result)
+ self.assertIn(16, result)
+
+class TestSparseList(unittest.TestCase):
+ """Test SparseList functionality."""
+
+ def test_sparse_list_creation(self):
+ """Test SparseList creation and basic operations."""
+ data = {10: 'ten', 20: 'twenty', 30: 'thirty'}
+ sparse_list = SparseList(data)
+
+ self.assertEqual(len(sparse_list), 31) # 0 to 30
+ self.assertEqual(sparse_list[10], 'ten')
+ self.assertEqual(sparse_list[20], 'twenty')
+ self.assertEqual(sparse_list[30], 'thirty')
+ self.assertIsNone(sparse_list[15]) # Gap
+
+ def test_sparse_list_iteration(self):
+ """Test SparseList iteration."""
+ data = {1: 'one', 3: 'three', 5: 'five'}
+ sparse_list = SparseList(data)
+
+ # SparseList should iterate over keys, not all values
+ keys = list(sparse_list)
+ expected_keys = [1, 3, 5]
+ self.assertEqual(keys, expected_keys)
+
+ # Test itervalues method for getting values
+ values = list(sparse_list.itervalues())
+ expected_values = ['one', 'three', 'five']
+ self.assertEqual(values, expected_values)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/build_reference_ntfs.py b/tools/build_reference_ntfs.py
new file mode 100755
index 0000000..0537893
--- /dev/null
+++ b/tools/build_reference_ntfs.py
@@ -0,0 +1,289 @@
+#!/usr/bin/env python3
+"""Build reference NTFS filesystem image for E2E tests.
+
+This script creates a reference NTFS filesystem image by:
+1. Creating a loop-mounted NTFS filesystem
+2. Copying reference test files to it
+3. Unmounting and saving the image
+4. Computing checksums for both the image and source files
+5. Storing metadata for validation
+
+Usage:
+ python tools/build_reference_ntfs.py [--size SIZE_MB] [--output OUTPUT_PATH]
+"""
+
+import argparse
+import hashlib
+import json
+import logging
+import os
+import shutil
+import subprocess
+import tempfile
+from pathlib import Path
+from typing import Dict, List
+
+import gzip
+
+class NTFSImageBuilder:
+ """Builder for reference NTFS filesystem images."""
+
+ def __init__(self, size_mb: int = 100, output_path: str = None, compress: bool = True):
+ self.size_mb = size_mb
+ self.output_path = output_path or "tests/data/reference_ntfs.img"
+ self.metadata_path = self.output_path.replace('.img', '.json')
+ self.reference_files_dir = Path("tests/data/reference_files")
+ self.compress = compress
+
+ # Set up logging
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
+ self.logger = logging.getLogger(__name__)
+
+ def _check_requirements(self) -> None:
+ """Check if required tools are available."""
+ required_tools = ['mkfs.ntfs', 'losetup', 'mount', 'umount', 'sync']
+ missing_tools = []
+
+ for tool in required_tools:
+ if shutil.which(tool) is None:
+ missing_tools.append(tool)
+
+ if missing_tools:
+ raise RuntimeError(f"Missing required tools: {', '.join(missing_tools)}")
+
+ # Check if running as root (needed for loop devices)
+ if os.geteuid() != 0:
+ raise RuntimeError("This script must be run as root to create loop devices")
+
+ def _compute_file_hash(self, filepath: Path) -> str:
+ """Compute SHA256 hash of a file."""
+ sha256_hash = hashlib.sha256()
+ with open(filepath, "rb") as f:
+ for chunk in iter(lambda: f.read(4096), b""):
+ sha256_hash.update(chunk)
+ return sha256_hash.hexdigest()
+
+ def _compute_directory_hash(self, directory: Path) -> Dict[str, str]:
+ """Compute hashes for all files in a directory recursively."""
+ file_hashes = {}
+
+ for filepath in directory.rglob('*'):
+ if filepath.is_file():
+ relative_path = filepath.relative_to(directory)
+ file_hashes[str(relative_path)] = self._compute_file_hash(filepath)
+ self.logger.info(f"Hashed {relative_path}: {file_hashes[str(relative_path)][:16]}...")
+
+ return file_hashes
+
+ def _create_empty_image(self, image_path: Path) -> None:
+ """Create an empty disk image file."""
+ self.logger.info(f"Creating {self.size_mb}MB empty image at {image_path}")
+
+ with open(image_path, 'wb') as f:
+ f.seek(self.size_mb * 1024 * 1024 - 1)
+ f.write(b'\0')
+
+ def _format_ntfs(self, image_path: Path) -> None:
+ """Format the image as NTFS."""
+ self.logger.info("Formatting image as NTFS...")
+
+ cmd = ['mkfs.ntfs', '-F', '-f', str(image_path)]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+
+ if result.returncode != 0:
+ raise RuntimeError(f"Failed to format NTFS: {result.stderr}")
+
+ def _setup_loop_device(self, image_path: Path) -> str:
+ """Set up loop device for the image."""
+ self.logger.info("Setting up loop device...")
+
+ cmd = ['losetup', '--find', '--show', str(image_path)]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+
+ if result.returncode != 0:
+ raise RuntimeError(f"Failed to set up loop device: {result.stderr}")
+
+ loop_device = result.stdout.strip()
+ self.logger.info(f"Created loop device: {loop_device}")
+ return loop_device
+
+ def _cleanup_loop_device(self, loop_device: str) -> None:
+ """Clean up loop device."""
+ self.logger.info(f"Cleaning up loop device: {loop_device}")
+
+ cmd = ['losetup', '-d', loop_device]
+ subprocess.run(cmd, capture_output=True, text=True)
+
+ def _mount_filesystem(self, loop_device: str, mount_point: Path) -> None:
+ """Mount the NTFS filesystem."""
+ self.logger.info(f"Mounting {loop_device} at {mount_point}")
+
+ cmd = ['mount', '-t', 'ntfs-3g', '-o', 'sync', loop_device, str(mount_point)]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+
+ if result.returncode != 0:
+ raise RuntimeError(f"Failed to mount filesystem: {result.stderr}")
+
+ def _unmount_filesystem(self, mount_point: Path) -> None:
+ """Unmount the filesystem."""
+ self.logger.info(f"Unmounting {mount_point}")
+
+ cmd = ['umount', str(mount_point)]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+
+ if result.returncode != 0:
+ self.logger.warning(f"Failed to unmount cleanly: {result.stderr}")
+
+ def _copy_files(self, mount_point: Path) -> None:
+ """Copy reference files to the mounted filesystem."""
+ self.logger.info("Copying reference files to mounted filesystem...")
+
+ if not self.reference_files_dir.exists():
+ raise RuntimeError(f"Reference files directory not found: {self.reference_files_dir}")
+
+ # Copy all files and directories
+ for item in self.reference_files_dir.iterdir():
+ dest = mount_point / item.name
+
+ if item.is_file():
+ shutil.copy2(item, dest)
+ self.logger.info(f"Copied file: {item.name}")
+ elif item.is_dir():
+ shutil.copytree(item, dest)
+ self.logger.info(f"Copied directory: {item.name}")
+
+ # Create alternate data stream (if supported)
+ try:
+ ads_file = mount_point / "file_with_ads.txt"
+ if ads_file.exists():
+ # Try to create ADS using attr command if available
+ if shutil.which('attr'):
+ cmd = ['attr', '-s', 'stream1', '-V', 'ADS content', str(ads_file)]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ if result.returncode == 0:
+ self.logger.info("Created alternate data stream")
+ else:
+ self.logger.warning("Failed to create ADS, not supported")
+ else:
+ self.logger.warning("attr tool not available, skipping ADS creation")
+ except Exception as e:
+ self.logger.warning(f"Could not create alternate data stream: {e}")
+
+ def _save_metadata(self, image_path: Path, file_hashes: Dict[str, str]) -> None:
+ """Save metadata about the image and source files."""
+ self.logger.info("Computing image hash and saving metadata...")
+
+ # Compute image hash
+ image_hash = self._compute_file_hash(image_path)
+
+ # Get image size
+ image_size = image_path.stat().st_size
+
+ metadata = {
+ "version": "1.0",
+ "created_by": "build_reference_ntfs.py",
+ "size_mb": self.size_mb,
+ "image_size_bytes": image_size,
+ "image_hash": image_hash,
+ "reference_files_hashes": file_hashes,
+ "file_count": len(file_hashes),
+ "notes": "Reference NTFS filesystem for RecuperaBit E2E tests"
+ }
+
+ with open(self.metadata_path, 'w') as f:
+ json.dump(metadata, f, indent=2, sort_keys=True)
+
+ self.logger.info(f"Saved metadata to {self.metadata_path}")
+ self.logger.info(f"Image hash: {image_hash}")
+
+ def build(self) -> None:
+ """Build the reference NTFS image."""
+ self.logger.info("Starting NTFS reference image build...")
+
+ # Check requirements
+ self._check_requirements()
+
+ # Prepare paths
+ image_path = Path(self.output_path)
+ image_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Compute hashes of source files
+ self.logger.info("Computing hashes of reference files...")
+ file_hashes = self._compute_directory_hash(self.reference_files_dir)
+
+ loop_device = None
+ temp_mount = None
+
+ try:
+ # Create and format image
+ self._create_empty_image(image_path)
+ self._format_ntfs(image_path)
+
+ # Set up loop device
+ loop_device = self._setup_loop_device(image_path)
+
+ # Create temporary mount point
+ temp_mount = Path(tempfile.mkdtemp(prefix="ntfs_build_"))
+
+ # Mount, copy files, unmount
+ self._mount_filesystem(loop_device, temp_mount)
+ self._copy_files(temp_mount)
+
+ # Sync to ensure all data is written
+ subprocess.run(['sync', str(temp_mount)], check=True)
+
+ self._unmount_filesystem(temp_mount)
+
+ # Save metadata
+ self._save_metadata(image_path, file_hashes)
+
+ self.logger.info(f"Successfully created reference NTFS image: {image_path}")
+ self.logger.info(f"Image size: {image_path.stat().st_size / (1024*1024):.1f} MB")
+
+ # Compress image
+ if self.compress:
+ self.logger.info("Compressing image with gzip...")
+ with open(image_path, 'rb') as f_in, gzip.open(f"{image_path}.gz", 'wb') as f_out:
+ shutil.copyfileobj(f_in, f_out)
+ image_path.unlink() # Remove uncompressed image
+
+ finally:
+ # Clean up
+ if loop_device:
+ self._cleanup_loop_device(loop_device)
+
+ if temp_mount and temp_mount.exists():
+ shutil.rmtree(temp_mount)
+
+
+def main():
+ """Main entry point."""
+ parser = argparse.ArgumentParser(description="Build reference NTFS image for E2E tests")
+ parser.add_argument('--size', type=int, default=100,
+ help='Image size in MB (default: 100)')
+ parser.add_argument('--output', type=str,
+ default='tests/data/reference_ntfs.img',
+ help='Output image path (default: tests/data/reference_ntfs.img)')
+
+ args = parser.parse_args()
+
+ builder = NTFSImageBuilder(size_mb=args.size, output_path=args.output)
+
+ try:
+ builder.build()
+ print(f"\n✓ Success! Reference NTFS image created at: {args.output}")
+ print(f"✓ Metadata saved at: {args.output.replace('.img', '.json')}")
+ print("\nNext steps:")
+ print("1. Add the .img file to Git LFS: git lfs track '*.img'")
+ print("2. Commit both the image and metadata files")
+ print("3. The E2E tests will now use this reference image")
+
+ except Exception as e:
+ print(f"\n✗ Error: {e}")
+ return 1
+
+ return 0
+
+
+if __name__ == '__main__':
+ exit(main())