diff --git a/trace/cli.py b/trace/cli.py index d864f43..98703b1 100644 --- a/trace/cli.py +++ b/trace/cli.py @@ -69,7 +69,7 @@ def export_markdown(output_file: str = "export.md"): try: storage = Storage() - with open(output_file, "w") as f: + with open(output_file, "w", encoding='utf-8') as f: f.write("# Forensic Notes Export\n\n") f.write(f"Generated on: {time.ctime()}\n\n") diff --git a/trace/storage.py b/trace/storage.py index 219afbf..5d5653b 100644 --- a/trace/storage.py +++ b/trace/storage.py @@ -166,7 +166,7 @@ Attachment: invoice.pdf.exe (double extension trick) #email-forensics #phishing- if not self.data_file.exists(): return [] try: - with open(self.data_file, 'r') as f: + with open(self.data_file, 'r', encoding='utf-8') as f: data = json.load(f) return [Case.from_dict(c) for c in data] except (json.JSONDecodeError, IOError): @@ -176,8 +176,8 @@ Attachment: invoice.pdf.exe (double extension trick) #email-forensics #phishing- data = [c.to_dict() for c in self.cases] # Write to temp file then rename for atomic-ish write temp_file = self.data_file.with_suffix(".tmp") - with open(temp_file, 'w') as f: - json.dump(data, f, indent=2) + with open(temp_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) temp_file.replace(self.data_file) def add_case(self, case: Case): @@ -225,15 +225,15 @@ class StateManager: state["evidence_id"] = evidence_id # Atomic write: write to temp file then rename temp_file = self.state_file.with_suffix(".tmp") - with open(temp_file, 'w') as f: - json.dump(state, f) + with open(temp_file, 'w', encoding='utf-8') as f: + json.dump(state, f, ensure_ascii=False) temp_file.replace(self.state_file) def get_active(self) -> dict: if not self.state_file.exists(): return {"case_id": None, "evidence_id": None} try: - with open(self.state_file, 'r') as f: + with open(self.state_file, 'r', encoding='utf-8') as f: return json.load(f) except (json.JSONDecodeError, IOError): return {"case_id": None, "evidence_id": None} @@ -242,7 +242,7 @@ class StateManager: if not self.settings_file.exists(): return {"pgp_enabled": True} try: - with open(self.settings_file, 'r') as f: + with open(self.settings_file, 'r', encoding='utf-8') as f: return json.load(f) except (json.JSONDecodeError, IOError): return {"pgp_enabled": True} @@ -252,6 +252,6 @@ class StateManager: settings[key] = value # Atomic write: write to temp file then rename temp_file = self.settings_file.with_suffix(".tmp") - with open(temp_file, 'w') as f: - json.dump(settings, f) + with open(temp_file, 'w', encoding='utf-8') as f: + json.dump(settings, f, ensure_ascii=False) temp_file.replace(self.settings_file) diff --git a/trace/tui.py b/trace/tui.py index 12440ec..b892ae9 100644 --- a/trace/tui.py +++ b/trace/tui.py @@ -1898,7 +1898,7 @@ class TUI: line = lines[cursor_line] lines[cursor_line] = line[:cursor_col] + chr(ch) + line[cursor_col:] cursor_col += 1 - + # Auto-wrap to next line if cursor exceeds visible width if cursor_col >= input_width: # Always ensure there's a next line to move to @@ -1911,6 +1911,42 @@ class TUI: if cursor_line >= scroll_offset + input_height: scroll_offset = cursor_line - input_height + 1 + elif ch > 127: + # UTF-8 multi-byte character (umlauts, etc.) + # curses returns the first byte, we need to read the rest + try: + # Try to decode as UTF-8 + # For multibyte UTF-8, we need to collect all bytes + bytes_collected = [ch] + + # Determine how many bytes we need based on the first byte + if ch >= 0xF0: # 4-byte character + num_bytes = 4 + elif ch >= 0xE0: # 3-byte character + num_bytes = 3 + elif ch >= 0xC0: # 2-byte character + num_bytes = 2 + else: + num_bytes = 1 + + # Read remaining bytes + for _ in range(num_bytes - 1): + next_ch = win.getch() + bytes_collected.append(next_ch) + + # Convert to character + char_bytes = bytes([b & 0xFF for b in bytes_collected]) + char = char_bytes.decode('utf-8') + + # Insert character at cursor + line = lines[cursor_line] + lines[cursor_line] = line[:cursor_col] + char + line[cursor_col:] + cursor_col += 1 + + except (UnicodeDecodeError, ValueError): + # If decode fails, ignore the character + pass + def dialog_confirm(self, message): curses.curs_set(0) h = 5 @@ -2643,7 +2679,7 @@ class TUI: # Write to file try: - with open(filepath, 'w') as f: + with open(filepath, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) self.show_message(f"IOCs exported to: {filepath}") except Exception as e: