Skip to content

Commit d4e4263

Browse files
committed
Implemented a check for larger base64 encoded blobs
1 parent 3966572 commit d4e4263

8 files changed

Lines changed: 138 additions & 27 deletions

file_utils.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99
import plain_spec
1010
from plain2code_console import console
11-
from plain2code_exceptions import UnsupportedResourceType
11+
from plain2code_exceptions import UnsupportedBase64Content, UnsupportedResourceType
1212
from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
13+
from plain2code_utils import find_large_base64_blob
1314
from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER, CODEPLAIN_METADATA_FOLDER
1415

1516
BINARY_FILE_EXTENSIONS = [".pyc"]
@@ -196,15 +197,26 @@ def load_linked_resources(template_dirs: list[str], resources_list, module_name:
196197
)
197198

198199
if content is None:
199-
raise FileNotFoundError(f"""
200+
raise FileNotFoundError(
201+
f"""
200202
Resource file {file_name} not found. Resource files are searched in the following order (highest to lowest precedence):
201203
202204
1. The directory containing your .plain file
203205
2. The directory specified by --template-dir (if provided)
204206
3. The built-in 'standard_template_library' directory
205207
206208
Please ensure that the resource exists in one of these locations, or specify the correct --template-dir if using custom templates.
207-
""")
209+
"""
210+
)
211+
212+
blob = find_large_base64_blob(content)
213+
if blob is not None:
214+
raise UnsupportedBase64Content(
215+
f"Referenced resource '{file_name}' in module '{module_name}' contains a large "
216+
f"base64-encoded blob ({len(blob)} characters), such as an embedded image. Inline "
217+
"base64 data is not supported. Remove the base64 data "
218+
"from the resource. If necessary, provide the binary file path in the specification."
219+
)
208220

209221
linked_resources[file_name] = content
210222

plain2code.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
PlainSyntaxError,
3838
RenderCancelledError,
3939
RenderingCreditBalanceTooLow,
40+
UnsupportedBase64Content,
4041
UnsupportedResourceType,
4142
)
4243
from plain2code_logger import (
@@ -74,6 +75,7 @@
7475
NetworkConnectionError,
7576
ModuleDoesNotExistError,
7677
UnsupportedResourceType,
78+
UnsupportedBase64Content,
7779
GitNotInstalledError,
7880
SystemExit,
7981
)

plain2code_exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class PlainSyntaxError(Exception):
2929
pass
3030

3131

32+
class UnsupportedBase64Content(Exception):
33+
34+
pass
35+
36+
3237
class InternalClientError(Exception):
3338
pass
3439

plain2code_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
import re
2+
from typing import Optional
3+
4+
#
5+
MIN_BASE64_BLOB_LENGTH = 8192
6+
7+
# Matches a long contiguous base64 / base64url run, optionally preceded by a data: URI header.
8+
_BASE64_BLOB_PATTERN = re.compile(
9+
r"(?:data:[\w.+-]+/[\w.+-]+;base64,)?[A-Za-z0-9+/_-]{%d,}={0,2}" % MIN_BASE64_BLOB_LENGTH
10+
)
11+
12+
13+
def find_large_base64_blob(text: str) -> Optional[str]:
14+
"""Return the first contiguous base64 blob at or above the threshold, or None."""
15+
match = _BASE64_BLOB_PATTERN.search(text)
16+
return match.group(0) if match else None
17+
18+
119
def format_duration_hms(total_seconds: int) -> str:
220
"""Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``)."""
321
if total_seconds < 0:

plain_file.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
import concept_utils
1818
import file_utils
1919
import plain_spec
20-
from plain2code_exceptions import ModuleDoesNotExistError, PlainSyntaxError
20+
from plain2code_exceptions import ModuleDoesNotExistError, PlainSyntaxError, UnsupportedBase64Content
2121
from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
22+
from plain2code_utils import find_large_base64_blob
2223

2324
RESOURCE_MARKER = "[resource]"
2425

@@ -528,6 +529,16 @@ def read_module_plain_source(module_name: str, template_dirs: list[str]) -> str:
528529
plain_source_text = file_utils.open_from(template_dirs, module_name + PLAIN_SOURCE_FILE_EXTENSION)
529530
if plain_source_text is None:
530531
raise ModuleDoesNotExistError(f"Module does not exist ({module_name}).")
532+
533+
blob = find_large_base64_blob(plain_source_text)
534+
if blob is not None:
535+
raise UnsupportedBase64Content(
536+
f"Module '{module_name}' contains a base64-encoded blob ({len(blob)} characters) "
537+
"inlined in the specification. This is not supported."
538+
"Remove the base64 data from the .plain file or if necessary,"
539+
"include the binary file path in the specification."
540+
)
541+
531542
return plain_source_text
532543

533544

tests/data/sample_base64_image.txt

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

tests/test_file_utils.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import pytest
55

66
from file_utils import load_linked_resources
7-
from plain2code_exceptions import UnsupportedResourceType
7+
from plain2code_exceptions import UnsupportedBase64Content, UnsupportedResourceType
8+
from plain2code_utils import MIN_BASE64_BLOB_LENGTH
89

910

1011
@pytest.fixture
@@ -39,3 +40,42 @@ def test_load_linked_resources_binary_file_raises_unsupported_resource_type(temp
3940
def test_load_linked_resources_missing_file_raises_file_not_found(template_dir):
4041
with pytest.raises(FileNotFoundError):
4142
load_linked_resources([template_dir], [{"text": "Missing", "target": "missing.md"}], "my_thing")
43+
44+
45+
def test_load_linked_resources_base64_blob_raises(template_dir):
46+
file_path = os.path.join(template_dir, "request.txt")
47+
with open(file_path, "w") as f:
48+
f.write("curl -d 'selfie_image=" + "A" * (MIN_BASE64_BLOB_LENGTH + 100) + "'")
49+
50+
with pytest.raises(UnsupportedBase64Content) as exc_info:
51+
load_linked_resources([template_dir], [{"text": "Request", "target": "request.txt"}], "my_thing")
52+
53+
assert "request.txt" in str(exc_info.value)
54+
assert "my_thing" in str(exc_info.value)
55+
56+
57+
def test_load_linked_resources_small_base64_allowed(template_dir):
58+
file_path = os.path.join(template_dir, "token.txt")
59+
with open(file_path, "w") as f:
60+
f.write("token=" + "A" * (MIN_BASE64_BLOB_LENGTH - 100))
61+
62+
result = load_linked_resources([template_dir], [{"text": "Token", "target": "token.txt"}], "my_thing")
63+
64+
assert "token.txt" in result
65+
66+
67+
def test_load_linked_resources_real_base64_image_raises(template_dir):
68+
# Real base64-encoded JPEG that made Gemini 3 fail, inlined into a curl example resource.
69+
sample_path = os.path.join(os.path.dirname(__file__), "data", "sample_base64_image.txt")
70+
with open(sample_path) as f:
71+
blob = f.read()
72+
73+
file_path = os.path.join(template_dir, "face_match_request.txt")
74+
with open(file_path, "w") as f:
75+
f.write(f"curl -d 'selfie_image={blob}' https://example.com/face-match")
76+
77+
with pytest.raises(UnsupportedBase64Content) as exc_info:
78+
load_linked_resources([template_dir], [{"text": "Request", "target": "face_match_request.txt"}], "face_match")
79+
80+
assert "face_match_request.txt" in str(exc_info.value)
81+
assert str(len(blob)) in str(exc_info.value)

tests/test_git_utils.py

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,17 @@ def test_single_file_change(temp_repo):
6565
result = diff(temp_repo, "1.1")
6666

6767
assert "test.txt" in result
68-
expected_diff = dedent("""
68+
expected_diff = dedent(
69+
"""
6970
--- a/test.txt
7071
+++ b/test.txt
7172
@@ -1,3 +1,3 @@
7273
-initial content
7374
+modified content
7475
line2
7576
line3
76-
""").strip()
77+
"""
78+
).strip()
7779
assert result["test.txt"] == expected_diff
7880

7981

@@ -98,27 +100,31 @@ def test_multiple_file_changes(temp_repo):
98100

99101
# Check first file
100102
assert "test.txt" in result
101-
expected_diff1 = dedent("""
103+
expected_diff1 = dedent(
104+
"""
102105
--- a/test.txt
103106
+++ b/test.txt
104107
@@ -1,3 +1,2 @@
105108
-initial content
106109
+file1 modified
107110
line2
108111
-line3
109-
""").strip()
112+
"""
113+
).strip()
110114
assert result["test.txt"] == expected_diff1
111115

112116
# Check second file
113117
assert "file2.txt" in result
114-
expected_diff2 = dedent("""
118+
expected_diff2 = dedent(
119+
"""
115120
--- /dev/null
116121
+++ b/file2.txt
117122
@@ -0,0 +1,2 @@
118123
+file2 modified
119124
+line2
120125
\\ No newline at end of file
121-
""").strip()
126+
"""
127+
).strip()
122128
assert result["file2.txt"] == expected_diff2
123129

124130

@@ -153,38 +159,44 @@ def test_multiple_commits_diff(temp_repo):
153159

154160
# Check first file
155161
assert "test.txt" in result
156-
expected_diff1 = dedent("""
162+
expected_diff1 = dedent(
163+
"""
157164
--- a/test.txt
158165
+++ b/test.txt
159166
@@ -1,3 +1,2 @@
160167
-initial content
161168
+file1 frid1.2 refactored version
162169
line2
163170
-line3
164-
""").strip()
171+
"""
172+
).strip()
165173
assert result["test.txt"] == expected_diff1
166174

167175
# Check second file
168176
assert "file2.txt" in result
169-
expected_diff2 = dedent("""
177+
expected_diff2 = dedent(
178+
"""
170179
--- a/file2.txt
171180
+++ b/file2.txt
172181
@@ -1,2 +1,2 @@
173182
-file2 frid1.1 refactored version
174183
+file2 frid1.2 version
175184
line2
176-
""").strip()
185+
"""
186+
).strip()
177187
assert result["file2.txt"] == expected_diff2
178188

179189
# Check third file
180190
assert "file3.txt" in result
181-
expected_diff3 = dedent("""
191+
expected_diff3 = dedent(
192+
"""
182193
--- /dev/null
183194
+++ b/file3.txt
184195
@@ -0,0 +1,2 @@
185196
+file3 frid1.2 new file
186197
+line2
187-
""").strip()
198+
"""
199+
).strip()
188200
assert result["file3.txt"] == expected_diff3
189201

190202

@@ -203,23 +215,27 @@ def test_diff_without_previous_frid_and_no_base_folder(empty_repo):
203215
result = diff(empty_repo)
204216

205217
assert "new.txt" in result
206-
expected_diff = dedent("""
218+
expected_diff = dedent(
219+
"""
207220
--- /dev/null
208221
+++ b/new.txt
209222
@@ -0,0 +1,2 @@
210223
+new file content
211224
+line2
212-
""").strip()
225+
"""
226+
).strip()
213227
assert result["new.txt"] == expected_diff
214228

215229
assert "new2.txt" in result
216-
expected_diff2 = dedent("""
230+
expected_diff2 = dedent(
231+
"""
217232
--- /dev/null
218233
+++ b/new2.txt
219234
@@ -0,0 +1,2 @@
220235
+new file content
221236
+line2
222-
""").strip()
237+
"""
238+
).strip()
223239
assert result["new2.txt"] == expected_diff2
224240

225241

@@ -237,14 +253,16 @@ def test_diff_without_previous_frid_and_base_folder(temp_repo):
237253
result = diff(temp_repo)
238254

239255
assert "new.txt" in result
240-
expected_diff = dedent("""
256+
expected_diff = dedent(
257+
"""
241258
--- a/new.txt
242259
+++ b/new.txt
243260
@@ -1,2 +1,2 @@
244261
-base folder content
245262
+updated base folder content
246263
line2
247-
""").strip()
264+
"""
265+
).strip()
248266
assert result["new.txt"] == expected_diff
249267

250268

@@ -260,13 +278,15 @@ def test_new_file(temp_repo):
260278
result = diff(temp_repo, "1.1")
261279

262280
assert "new.txt" in result
263-
expected_diff = dedent("""
281+
expected_diff = dedent(
282+
"""
264283
--- /dev/null
265284
+++ b/new.txt
266285
@@ -0,0 +1,2 @@
267286
+new file content
268287
+line2
269-
""").strip()
288+
"""
289+
).strip()
270290
assert result["new.txt"] == expected_diff
271291

272292

@@ -282,14 +302,16 @@ def test_deleted_file(temp_repo):
282302
result = diff(temp_repo, "1.1")
283303

284304
assert "test.txt" in result
285-
expected_diff = dedent("""
305+
expected_diff = dedent(
306+
"""
286307
--- a/test.txt
287308
+++ /dev/null
288309
@@ -1,3 +0,0 @@
289310
-initial content
290311
-line2
291312
-line3
292-
""").strip()
313+
"""
314+
).strip()
293315
assert result["test.txt"] == expected_diff
294316

295317

0 commit comments

Comments
 (0)