diff --git a/freedium-library/src/freedium_library/utils/utils/__init__.py b/freedium-library/src/freedium_library/utils/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freedium-library/src/freedium_library/utils/utils/mutable_string.py b/freedium-library/src/freedium_library/utils/utils/mutable_string.py index f791a94..68c3666 100644 --- a/freedium-library/src/freedium_library/utils/utils/mutable_string.py +++ b/freedium-library/src/freedium_library/utils/utils/mutable_string.py @@ -32,6 +32,8 @@ class MutableString: return self.string.encode(encoding, "surrogatepass") def insert(self, key: int, value: str) -> "MutableString": + if key < 0 or key > len(self._string_list): + raise IndexError("string index out of range") self._string_list.insert(key, value) self._mark_dirty() return self @@ -50,4 +52,7 @@ class MutableString: return self.string def delete(self, start: int, length: int) -> None: + if start < 0 or start >= len(self._string_list): + raise IndexError("string index out of range") del self._string_list[start : start + length] + self._mark_dirty() diff --git a/freedium-library/src/freedium_library/utils/utils/test_mutable_string.py b/freedium-library/src/freedium_library/utils/utils/test_mutable_string.py new file mode 100644 index 0000000..132a8bb --- /dev/null +++ b/freedium-library/src/freedium_library/utils/utils/test_mutable_string.py @@ -0,0 +1,87 @@ +import pytest + +from freedium_library.utils.utils.mutable_string import MutableString + + +def test_init(): + # Test basic string initialization + ms = MutableString("hello") + assert str(ms) == "hello" + + # Test initialization from another MutableString + ms2 = MutableString(ms) + assert str(ms2) == "hello" + + # Test empty string + ms3 = MutableString("") + assert str(ms3) == "" + + +def test_string_property(): + ms = MutableString("test") + ms._string_list[0] = "b" + ms._mark_dirty() + assert ms.string == "best" + + +def test_len(): + ms = MutableString("hello") + assert len(ms) == 5 + assert len(MutableString("")) == 0 + + +def test_pop(): + ms = MutableString("hello") + ms.pop(0) + assert str(ms) == "ello" + + with pytest.raises(IndexError): + ms.pop(10) + + +def test_encode(): + ms = MutableString("hello") + assert ms.encode("utf-8") == b"hello" + + # Test unicode + ms = MutableString("hello 🌍") + assert ms.encode("utf-8").decode("utf-8") == "hello 🌍" + + +def test_insert(): + ms = MutableString("hello") + ms.insert(0, "x") + assert str(ms) == "xhello" + + ms.insert(len(ms), "y") + assert str(ms) == "xhelloy" + + with pytest.raises(IndexError): + ms.insert(100, "z") + + +def test_setitem(): + ms = MutableString("hello") + ms[0] = "j" + assert str(ms) == "jello" + + # Test slice + ms[1:4] = "a" + assert str(ms) == "jao" + + +def test_getitem(): + ms = MutableString("hello") + assert ms[0] == "h" + + with pytest.raises(IndexError): + _ = ms[10] + + +def test_delete(): + ms = MutableString("hello") + ms.delete(1, 2) + assert str(ms) == "hlo" + + with pytest.raises(IndexError): + ms.delete(10, 1) diff --git a/freedium-library/src/freedium_library/utils/utils/utf_handler.py b/freedium-library/src/freedium_library/utils/utils/utf_handler.py index 5dabe17..759425d 100644 --- a/freedium-library/src/freedium_library/utils/utils/utf_handler.py +++ b/freedium-library/src/freedium_library/utils/utils/utf_handler.py @@ -1,7 +1,6 @@ from abc import ABC from dataclasses import dataclass from enum import Enum, auto -from functools import cached_property from typing import List, Optional from loguru import logger @@ -16,19 +15,25 @@ class UTFEncoding(Enum): @property def name(self) -> str: - return { + logger.trace(f"Getting encoding name for {self}") + name = { UTFEncoding.UTF8: "utf-8", UTFEncoding.UTF16: "utf-16-le", UTFEncoding.UTF32: "utf-32-le", }[self] + logger.trace(f"Encoding name resolved to {name}") + return name @property def unit_size(self) -> int: - return { + logger.trace(f"Getting unit size for encoding {self}") + size = { UTFEncoding.UTF8: 1, UTFEncoding.UTF16: 2, UTFEncoding.UTF32: 4, }[self] + logger.trace(f"Unit size resolved to {size}") + return size @dataclass @@ -41,16 +46,28 @@ class CharacterMapping: original_char_length: int char_length: int + def __post_init__(self): + logger.trace(f"Validating CharacterMapping for char '{self.char}'") + if self.original_pos < 0 or self.current_pos < 0: + logger.error( + f"Invalid negative position: original_pos={self.original_pos}, current_pos={self.current_pos}" + ) + raise ValueError("Positions cannot be negative") + if self.char_length < 1: + logger.error(f"Invalid character length: {self.char_length}") + raise ValueError("Character length must be positive") + logger.trace("CharacterMapping validation successful") + def shift_positions(self, offset: int, encoded_offset: int) -> None: logger.trace( - f"Shifting positions: offset={offset}, encoded_offset={encoded_offset}" + f"Shifting positions for char '{self.char}': offset={offset}, encoded_offset={encoded_offset}" ) - logger.trace( + logger.debug( f"Before shift: current_pos={self.current_pos}, encoded_pos={self.encoded_pos}" ) self.current_pos += offset self.encoded_pos += encoded_offset - logger.trace( + logger.debug( f"After shift: current_pos={self.current_pos}, encoded_pos={self.encoded_pos}" ) @@ -68,8 +85,10 @@ class PositionTracker: def __init__(self): """Initialize empty position tracker.""" + logger.debug("Initializing new PositionTracker") self._position_mappings = [] self._original_positions = [] + logger.trace("PositionTracker initialized with empty mappings") def add(self, mapping: CharacterMapping, insert_idx: Optional[int] = None) -> None: """Add a new character mapping. @@ -78,10 +97,16 @@ class PositionTracker: mapping: CharacterMapping object to add insert_idx: Optional index to insert at specific position """ + logger.debug( + f"Adding new mapping for char '{mapping.char}' at position {mapping.current_pos}" + ) if insert_idx is None: + logger.trace("Appending mapping to end of list") self._position_mappings.append(mapping) else: + logger.trace(f"Inserting mapping at index {insert_idx}") self._position_mappings.insert(insert_idx, mapping) + logger.debug(f"Current mapping count: {len(self._position_mappings)}") def get(self) -> List[CharacterMapping]: """Get copy of all character mappings. @@ -89,6 +114,8 @@ class PositionTracker: Returns: List of CharacterMapping objects """ + logger.trace("Getting copy of all character mappings") + logger.debug(f"Returning {len(self._position_mappings)} mappings") return self._position_mappings.copy() def clear(self, start: int, length: int) -> int: @@ -101,13 +128,20 @@ class PositionTracker: Returns: Number of mappings that were cleared """ + logger.debug(f"Clearing mappings from position {start} to {start + length}") original_count = len(self._position_mappings) + logger.trace(f"Original mapping count: {original_count}") + self._position_mappings = [ mapping for mapping in self._position_mappings if not (start <= mapping.original_pos < start + length) ] - return original_count - len(self._position_mappings) + + cleared_count = original_count - len(self._position_mappings) + logger.debug(f"Cleared {cleared_count} mappings") + logger.trace(f"Remaining mappings: {len(self._position_mappings)}") + return cleared_count def update(self, start: int, length: int, encoded_length: int) -> None: """Update positions of mappings after a modification. @@ -117,87 +151,79 @@ class PositionTracker: length: Length of original content modified encoded_length: Length of new encoded content """ + logger.debug( + f"Updating mappings: start={start}, length={length}, encoded_length={encoded_length}" + ) for mapping in self._position_mappings: if mapping.original_pos >= start + length: + logger.trace(f"Updating mapping for char '{mapping.char}'") + logger.trace( + f"Before update: original_pos={mapping.original_pos}, encoded_pos={mapping.encoded_pos}" + ) mapping.original_pos -= length mapping.encoded_pos -= encoded_length + logger.trace( + f"After update: original_pos={mapping.original_pos}, encoded_pos={mapping.encoded_pos}" + ) class UTFHandler(ABC): __slots__ = ("_string", "_encoding", "_position_tracker") def __init__(self, string: str, encoding: UTFEncoding): - logger.debug( - f"Initializing UTFHandler with string: '{string}' and encoding: {encoding}" - ) - logger.trace(f"String length: {len(string)}") - logger.trace(f"Encoding: {encoding}") - + logger.info(f"Initializing UTFHandler with encoding {encoding}") + logger.debug(f"Input string length: {len(string)}") self._string = MutableString(string) self._encoding = encoding self._position_tracker = PositionTracker() - - logger.trace("Calling _initialize_mappings()") + logger.debug("Initializing character mappings") self._initialize_mappings() - - @cached_property - def _string_len(self) -> int: - length = len(self._string) - logger.trace(f"Calculating string length: {length}") - return length - - @cached_property - def _encoded_len(self) -> int: - logger.trace("Calculating encoded length") - encoded_bytes = len(self._string.encode(self._encoding.name)) - logger.trace(f"Encoded bytes: {encoded_bytes}") - encoded_len = encoded_bytes // self._encoding.unit_size - logger.trace(f"Encoded length: {encoded_len}") - return encoded_len + logger.info("UTFHandler initialization complete") def _initialize_mappings(self) -> None: - logger.debug("Starting character mappings initialization") + logger.debug("Beginning mapping initialization") current_pos = 0 encoded_pos = 0 - for i in range(len(self._string)): - char = self._string[i] - char_len = len(char.encode(self._encoding.name)) // self._encoding.unit_size + for i, char in enumerate(self._string): + logger.trace(f"Processing character '{char}' at position {i}") + encoded_bytes = char.encode(self._encoding.name) + char_len = len(encoded_bytes) // self._encoding.unit_size + logger.trace(f"Encoded length: {char_len} units") if char_len > 1: logger.debug(f"Found multi-byte character '{char}' at position {i}") mapping = CharacterMapping( + char=char, original_pos=i, current_pos=current_pos, encoded_pos=encoded_pos, char_length=char_len, original_char_length=len(char), original_encoded_pos=encoded_pos, - char=char, ) + logger.trace(f"Created mapping: {mapping}") self._position_tracker.add(mapping) current_pos += 1 encoded_pos += char_len + logger.trace( + f"Updated positions: current={current_pos}, encoded={encoded_pos}" + ) + + logger.debug("Mapping initialization complete") def get_encoded_position(self, original_pos: int) -> int: - logger.debug(f"Converting original position {original_pos} to encoded position") encoded_pos = original_pos for mapping in self._position_tracker.get(): if mapping.original_pos < original_pos: encoded_pos += mapping.char_length - 1 - logger.trace( - f"Adjusting for mapping at {mapping.original_pos}, new encoded_pos: {encoded_pos}" - ) elif mapping.original_pos == original_pos: - encoded_pos = mapping.encoded_pos - logger.debug(f"Direct mapping found, encoded_pos set to {encoded_pos}") - break + return mapping.encoded_pos else: break - logger.debug(f"Final encoded position: {encoded_pos}") return encoded_pos def get_original_position(self, encoded_pos: int) -> int: @@ -205,9 +231,14 @@ class UTFHandler(ABC): original_pos = encoded_pos for mapping in self._position_tracker.get(): + logger.trace( + f"Checking mapping for char '{mapping.char}' at encoded position {mapping.encoded_pos}" + ) if mapping.encoded_pos < encoded_pos: + logger.trace( + f"Adjusting for multi-byte character: -{mapping.char_length + 1}" + ) original_pos -= mapping.char_length + 1 - logger.trace(f"Adjusting original position: {original_pos}") else: break @@ -225,33 +256,35 @@ class UTFHandler(ABC): ) logger.debug(f"Encoded insertion length: {insert_encoded_len}") - # Find the original position that corresponds to the encoded position original_position = self.get_original_position(encoded_position) + logger.debug(f"Corresponding original position: {original_position}") - # Shift existing mappings that are after the insertion point + logger.debug("Updating existing mappings") for mapping in self._position_tracker.get(): if mapping.encoded_pos >= encoded_position: + logger.trace(f"Shifting mapping for char '{mapping.char}'") mapping.shift_positions(len(string_to_insert), insert_encoded_len) - # Add new mappings for the inserted string + logger.debug("Adding mappings for inserted string") self._add_new_mappings(string_to_insert, original_position, encoded_position) - # Insert the string at the correct position + logger.debug(f"Inserting string at position {original_position}") self._string.insert(original_position, string_to_insert) - logger.debug(f"Insert complete. New string: '{self._string}'") + logger.info(f"Insert complete. New string: '{self._string}'") def _add_new_mappings( self, string_to_insert: str, original_position: int, encoded_position: int ) -> None: - logger.debug("Adding new mappings for inserted string") + logger.debug(f"Adding new mappings for string '{string_to_insert}'") current_pos = original_position encoded_pos = encoded_position for char in string_to_insert: + logger.trace(f"Processing character '{char}'") encoded_char_len = ( len(char.encode(self._encoding.name)) // self._encoding.unit_size ) - logger.trace(f"Processing char '{char}' with length {encoded_char_len}") + logger.trace(f"Encoded character length: {encoded_char_len}") if encoded_char_len > 1: logger.debug(f"Found multi-byte character '{char}'") @@ -265,7 +298,6 @@ class UTFHandler(ABC): char=char, ) - # Find the correct position to insert the new mapping insert_idx = next( ( i @@ -275,59 +307,85 @@ class UTFHandler(ABC): len(self._position_tracker.get()), ) - logger.trace(f"Inserting new mapping at index {insert_idx}") + logger.debug(f"Inserting new mapping at index {insert_idx}") self._position_tracker.add(new_mapping, insert_idx) current_pos += 1 encoded_pos += encoded_char_len + logger.trace( + f"Updated positions: current={current_pos}, encoded={encoded_pos}" + ) - logger.debug(f"New position mappings: {self._position_tracker.get()}") + logger.debug("Finished adding new mappings") def delete(self, start: int, length: int) -> None: - logger.info(f"Deleting {length} characters starting at position {start}") + logger.debug(f"Entering delete method with start={start}, length={length}") + logger.info( + f"Initiating deletion of {length} characters starting at position {start}" + ) + + logger.trace("Calculating encoded positions for deletion") start_encoded = self.get_encoded_position(start) - end_encoded = self.get_encoded_position(start + length) + logger.debug(f"Calculated start encoded position: {start_encoded}") + + end_pos = start + length + logger.trace(f"Calculated end position: {end_pos}") + + end_encoded = self.get_encoded_position(end_pos) + logger.debug(f"Calculated end encoded position: {end_encoded}") + encoded_length = end_encoded - start_encoded - logger.debug( - f"Encoded deletion range: {start_encoded} to {end_encoded} (length: {encoded_length})" - ) - - original_mapping_count = len(self._position_tracker.get()) - self._position_tracker.clear(start, length) - logger.debug( - f"Removed {original_mapping_count - len(self._position_tracker.get())} mappings" - ) + logger.info(f"Calculated encoded length to delete: {encoded_length}") + logger.trace("Updating remaining position mappings") for mapping in self._position_tracker.get(): - if mapping.original_pos >= start + length: - old_pos = mapping.original_pos - old_encoded = mapping.encoded_pos - mapping.original_pos -= length - mapping.encoded_pos -= encoded_length - logger.trace( - f"Updated mapping: {old_pos}->{mapping.original_pos}, {old_encoded}->{mapping.encoded_pos}" + if mapping.original_pos >= end_pos: + logger.debug( + f"Adjusting mapping for character at position {mapping.original_pos}" ) + mapping.original_pos -= length + mapping.current_pos -= length + mapping.encoded_pos -= encoded_length + mapping.original_encoded_pos -= encoded_length + logger.trace(f"Updated mapping: {mapping}") + logger.debug("Performing deletion on underlying string") self._string.delete(start, length) - logger.debug(f"Delete complete. New string: '{self._string}'") + logger.info( + f"Delete operation complete. New string length: {len(self._string)}" + ) + logger.trace(f"Updated string contents: '{self._string}'") def get_string_slice(self, start: int, end: int) -> List[str]: - logger.trace(f"Getting string slice from {start} to {end}") - return list(self._string[start:end]) + logger.debug(f"Getting string slice from {start} to {end}") + result = list(self._string[start:end]) + logger.trace(f"Slice result: {result}") + return result def __getitem__(self, key: int) -> str: - logger.trace(f"Getting character at index {key}") - return "".join(self._string[key]) - - @property - def encoded_length(self) -> int: - logger.trace("Retrieving encoded length") - return self._encoded_len + logger.debug(f"Getting character at index {key}") + result = "".join(self._string[key]) + logger.trace(f"Retrieved character: '{result}'") + return result def __str__(self) -> str: - logger.trace("Converting to string representation") - return str(self._string) + logger.debug("Converting to string representation") + result = str(self._string) + logger.trace(f"String representation: '{result}'") + return result def __repr__(self) -> str: - logger.trace("Getting detailed string representation") - return f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})" + logger.debug("Getting detailed string representation") + result = f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})" + logger.trace(f"Detailed representation: {result}") + return result + logger.debug("Converting to string representation") + result = str(self._string) + logger.trace(f"String representation: '{result}'") + return result + + def __repr__(self) -> str: + logger.debug("Getting detailed string representation") + result = f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})" + logger.trace(f"Detailed representation: {result}") + return result