Skip to content

Commit 8b97bfd

Browse files
committed
feat(json): keep extra top-level keys instead of dropping them
render() only emitted the known TOP_LEVEL_KEYS, so a future document with an additional top-level field would silently lose it. Append any extra keys after the fixed ordering (metadata first), preserving detect_file_type behavior. Add tests for the extra-key guard and the manual --language override.
1 parent 8ef4755 commit 8b97bfd

3 files changed

Lines changed: 65 additions & 3 deletions

File tree

floss/render/json.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,16 @@ def sort_nested(obj):
5959
TOP_LEVEL_KEYS = ("metadata", "analysis", "strings", "layout")
6060

6161

62-
def render(doc: ResultDocument) -> str:
63-
data = dataclasses.asdict(doc)
64-
top = {key: sort_nested(data[key]) for key in TOP_LEVEL_KEYS if key in data}
62+
def _render_dict(data: dict) -> str:
63+
"""serialize a results-document dict with fixed top-level ordering."""
64+
# keep the fixed top-level ordering (metadata first so the document stays
65+
# detectable from its leading bytes), then append any future/extra keys so
66+
# they are never silently dropped
67+
extra_keys = [key for key in data if key not in TOP_LEVEL_KEYS]
68+
ordered_keys = list(TOP_LEVEL_KEYS) + extra_keys
69+
top = {key: sort_nested(data[key]) for key in ordered_keys if key in data}
6570
return json.dumps(top, cls=FlossJSONEncoder, sort_keys=False)
71+
72+
73+
def render(doc: ResultDocument) -> str:
74+
return _render_dict(dataclasses.asdict(doc))

tests/test_cli_args.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,36 @@ def test_language_extraction_independent_of_static():
113113
assert len(results.strings.static_strings) == 0
114114

115115

116+
def test_manual_language_override_wins_over_auto_detect():
117+
"""--language go must be honored even when auto-detection returns unknown."""
118+
from pathlib import Path
119+
120+
from floss.results import Analysis
121+
from floss.pipeline import Options, analyze
122+
123+
# a C binary, so auto-detection yields unknown; forcing go must stick
124+
sample = Path(__file__).parent / "data" / "src" / "decode-in-place" / "bin" / "test-decode-in-place.exe"
125+
126+
results = analyze(
127+
Options(
128+
sample=sample,
129+
min_length=4,
130+
language="go",
131+
analysis=Analysis(
132+
enable_static_strings=False,
133+
enable_stack_strings=False,
134+
enable_tight_strings=False,
135+
enable_decoded_strings=False,
136+
enable_language_strings=True,
137+
),
138+
prompt_deobfuscation=False,
139+
)
140+
)
141+
assert results is not None
142+
assert results.metadata.language == "go"
143+
assert results.metadata.language_selected == "go"
144+
145+
116146
def test_expand_string_types():
117147
from floss.utils import expand_string_types
118148

tests/test_results_json_roundtrip.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import json
1818
import tempfile
19+
import dataclasses
1920
from pathlib import Path
2021

2122
import floss.utils
@@ -154,3 +155,25 @@ def test_rendered_document_is_detectable(tmp_path):
154155
p.write_text(floss.render.json.render(doc))
155156

156157
assert floss.utils.detect_file_type(p) is floss.utils.FileType.RESULTS
158+
159+
160+
def test_json_render_keeps_extra_top_level_keys():
161+
"""future/extra top-level fields must not be silently dropped."""
162+
from floss.render import json as render_json
163+
164+
doc = ResultDocument(
165+
metadata=Metadata(file_path="sample.exe", min_length=4),
166+
analysis=Analysis(),
167+
strings=Strings(),
168+
layout=None,
169+
)
170+
data = dataclasses.asdict(doc)
171+
# simulate a future document gaining an extra top-level field
172+
data["future_field"] = {"a": 1}
173+
174+
raw = render_json._render_dict(data)
175+
obj = json.loads(raw)
176+
assert obj["future_field"] == {"a": 1}
177+
# ordering preserved: metadata first, extras appended after known keys
178+
assert list(obj.keys())[0] == "metadata"
179+
assert list(obj.keys())[-1] == "future_field"

0 commit comments

Comments
 (0)