Skip to content

Commit 285406e

Browse files
Merge pull request #495 from mickey-mikey/fix/windows-attachment-corruption
Fix Windows attachment corruption: add O_BINARY flag to os.open
2 parents 724234e + 1e36375 commit 285406e

2 files changed

Lines changed: 96 additions & 1 deletion

File tree

core/attachment_storage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def save_attachment(
102102
# Save file with restrictive permissions (sensitive email/drive content)
103103
file_path = STORAGE_DIR / save_name
104104
try:
105-
fd = os.open(file_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
105+
fd = os.open(file_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, 'O_BINARY', 0), 0o600)
106106
try:
107107
total_written = 0
108108
data_len = len(file_bytes)

tests/gmail/test_attachment_fix.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import base64
2+
import os
3+
import sys
4+
5+
import pytest
6+
7+
8+
def test_urlsafe_b64decode_already_handles_crlf():
9+
"""Verify Python's urlsafe_b64decode ignores embedded CR/LF without manual stripping."""
10+
original = b"Testdata"
11+
b64 = base64.urlsafe_b64encode(original).decode()
12+
13+
assert base64.urlsafe_b64decode(b64 + "\n") == original
14+
assert base64.urlsafe_b64decode(b64[:4] + "\r\n" + b64[4:]) == original
15+
assert base64.urlsafe_b64decode(b64[:4] + "\r\r\n" + b64[4:]) == original
16+
17+
18+
def test_os_open_without_o_binary_corrupts_on_windows(tmp_path):
19+
"""On Windows, os.open without O_BINARY translates LF to CRLF in written bytes."""
20+
payload = b"\x89PNG\r\n\x1a\n" + b"\x00" * 50
21+
22+
tmp = str(tmp_path / "test_no_binary.bin")
23+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
24+
try:
25+
os.write(fd, payload)
26+
finally:
27+
os.close(fd)
28+
29+
with open(tmp, "rb") as f:
30+
written = f.read()
31+
32+
if sys.platform == "win32":
33+
assert written != payload, "Expected corruption without O_BINARY on Windows"
34+
assert len(written) > len(payload)
35+
else:
36+
assert written == payload
37+
38+
39+
def test_os_open_with_o_binary_preserves_bytes(tmp_path):
40+
"""os.open with O_BINARY writes binary data correctly on all platforms."""
41+
payload = b"\x89PNG\r\n\x1a\n" + b"\x00" * 50
42+
43+
tmp = str(tmp_path / "test_with_binary.bin")
44+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, 'O_BINARY', 0)
45+
46+
fd = os.open(tmp, flags, 0o600)
47+
try:
48+
os.write(fd, payload)
49+
finally:
50+
os.close(fd)
51+
52+
with open(tmp, "rb") as f:
53+
written = f.read()
54+
55+
assert written == payload
56+
57+
58+
@pytest.fixture
59+
def isolated_storage(tmp_path, monkeypatch):
60+
"""Create an AttachmentStorage that writes to a temp directory."""
61+
import core.attachment_storage as storage_module
62+
monkeypatch.setattr(storage_module, "STORAGE_DIR", tmp_path)
63+
return storage_module.AttachmentStorage()
64+
65+
66+
def test_save_attachment_uses_binary_mode(isolated_storage):
67+
"""Verify that AttachmentStorage.save_attachment writes files in binary mode."""
68+
payload = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
69+
b64_data = base64.urlsafe_b64encode(payload).decode()
70+
71+
result = isolated_storage.save_attachment(b64_data, filename="test.png", mime_type="image/png")
72+
73+
with open(result.path, "rb") as f:
74+
saved_bytes = f.read()
75+
76+
assert saved_bytes == payload, (
77+
f"Binary corruption detected: wrote {len(payload)} bytes, "
78+
f"read back {len(saved_bytes)} bytes"
79+
)
80+
81+
82+
@pytest.mark.parametrize("payload", [
83+
b"\x89PNG\r\n\x1a\n" + b"\xff" * 200, # PNG header
84+
b"%PDF-1.7\n" + b"\x00" * 200, # PDF header
85+
bytes(range(256)) * 4, # All byte values
86+
])
87+
def test_save_attachment_preserves_various_binary_formats(isolated_storage, payload):
88+
"""Ensure binary integrity for payloads containing LF/CR bytes."""
89+
b64_data = base64.urlsafe_b64encode(payload).decode()
90+
result = isolated_storage.save_attachment(b64_data, filename="test.bin")
91+
92+
with open(result.path, "rb") as f:
93+
saved_bytes = f.read()
94+
95+
assert saved_bytes == payload

0 commit comments

Comments
 (0)