11from __future__ import annotations
22
33import argparse
4+ import codecs
45import json
56import os
67import sys
4041)
4142from ...paths import resolve_runtime_root
4243
44+ MAX_INPUT_BYTES = 16 * 1024 * 1024
45+
46+
47+ def _input_byte_limit (text : str ) -> int :
48+ # An explicit, positive byte count; there is no unbounded setting.
49+ if not text .isdecimal () or int (text ) < 1 :
50+ raise argparse .ArgumentTypeError (f"must be a positive whole number of bytes, got { text !r} " )
51+ return int (text )
52+
53+
54+ def _add_input_limit (parser : argparse .ArgumentParser ) -> None :
55+ parser .add_argument (
56+ "--max-input-bytes" ,
57+ type = _input_byte_limit ,
58+ default = MAX_INPUT_BYTES ,
59+ help = (
60+ "Refuse a JSON input file or stdin larger than this many bytes before "
61+ f"parsing (default { MAX_INPUT_BYTES } ). Bounds input size, not process memory."
62+ ),
63+ )
64+
65+
66+ def _read_stdin_text (limit : int , label : str ) -> str :
67+ # Read through the text stream so stdin keeps its decoding, but charge each
68+ # chunk by its encoded size: one character can take up to four bytes. One
69+ # incremental encoder spans all chunks, so a BOM is charged only once.
70+ encoding = getattr (sys .stdin , "encoding" , None ) or "utf-8"
71+ errors = getattr (sys .stdin , "errors" , None ) or "surrogatepass"
72+ encoder = codecs .getincrementalencoder (encoding )(errors )
73+ chunks : list [str ] = []
74+ consumed = 0
75+ while consumed <= limit :
76+ chunk = sys .stdin .read (max (1 , (limit + 1 - consumed ) // 4 ))
77+ if not chunk :
78+ consumed += len (encoder .encode ("" , final = True ))
79+ break
80+ chunks .append (chunk )
81+ consumed += len (encoder .encode (chunk ))
82+ if consumed > limit :
83+ raise ValueError (f"{ label } exceeds the { limit } -byte limit" )
84+ return "" .join (chunks )
85+
86+
4387PrintPayload = Callable [
4488 [dict [str , object ], str , Callable [[dict [str , object ]], str ]],
4589 None ,
@@ -59,11 +103,17 @@ def __call__(
59103ProviderCommandHandler = Callable [..., int | None ]
60104
61105
62- def _load_json_object (path_text : str ) -> dict [str , Any ]:
106+ def _load_json_object (path_text : str , limit : int ) -> dict [str , Any ]:
107+ label = "periodic-report JSON input"
63108 if path_text == "-" :
64- payload = json . loads ( sys . stdin . read () )
109+ text = _read_stdin_text ( limit , label )
65110 else :
66- payload = json .loads (Path (path_text ).expanduser ().read_text (encoding = "utf-8" ))
111+ with Path (path_text ).expanduser ().open ("rb" ) as source :
112+ raw = source .read (limit + 1 )
113+ if len (raw ) > limit :
114+ raise ValueError (f"{ label } exceeds the { limit } -byte limit" )
115+ text = raw .decode ("utf-8" )
116+ payload = json .loads (text )
67117 if not isinstance (payload , dict ):
68118 raise ValueError (f"{ path_text } must contain a JSON object" )
69119 return payload
@@ -85,6 +135,7 @@ def register_periodic_report_commands(
85135 help = "Normalize one periodic_report_v0 attempt without provider effects." ,
86136 )
87137 add_subcommand_format (compose )
138+ _add_input_limit (compose )
88139 compose .add_argument (
89140 "--request-json" ,
90141 required = True ,
@@ -95,6 +146,7 @@ def register_periodic_report_commands(
95146 help = "Evaluate cadence and material progress triggers without effects." ,
96147 )
97148 add_subcommand_format (evaluate )
149+ _add_input_limit (evaluate )
98150 evaluate .add_argument (
99151 "--request-json" ,
100152 required = True ,
@@ -105,6 +157,7 @@ def register_periodic_report_commands(
105157 help = "Promote durable rollout events into a periodic-report trigger." ,
106158 )
107159 add_subcommand_format (evaluate_runtime )
160+ _add_input_limit (evaluate_runtime )
108161 evaluate_runtime .add_argument (
109162 "--request-json" ,
110163 required = True ,
@@ -153,6 +206,7 @@ def register_periodic_report_commands(
153206 help = "Preview or apply the runtime-root machine periodic-report policy." ,
154207 )
155208 add_subcommand_format (configure_machine_defaults )
209+ _add_input_limit (configure_machine_defaults )
156210 configure_machine_defaults .add_argument (
157211 "--config-json" ,
158212 required = True ,
@@ -187,6 +241,7 @@ def register_periodic_report_commands(
187241 ),
188242 )
189243 add_subcommand_format (plan_goal_delivery )
244+ _add_input_limit (plan_goal_delivery )
190245 plan_goal_delivery .add_argument (
191246 "--request-json" ,
192247 required = True ,
@@ -202,6 +257,7 @@ def register_periodic_report_commands(
202257 help = "Inspect a built-in preset or project profile without provider effects." ,
203258 )
204259 add_subcommand_format (inspect_profile )
260+ _add_input_limit (inspect_profile )
205261 profile_source = inspect_profile .add_mutually_exclusive_group (required = True )
206262 profile_source .add_argument (
207263 "--profile-json" ,
@@ -220,6 +276,7 @@ def register_periodic_report_commands(
220276 ),
221277 )
222278 add_subcommand_format (archive )
279+ _add_input_limit (archive )
223280 archive .add_argument (
224281 "--request-json" ,
225282 required = True ,
@@ -465,22 +522,22 @@ def handle_periodic_report_command(
465522 try :
466523 if args .periodic_report_command == "archive-openviking" :
467524 payload = _archive_openviking (
468- _load_json_object (args .request_json ),
525+ _load_json_object (args .request_json , args . max_input_bytes ),
469526 args ,
470527 )
471528 elif args .periodic_report_command == "inspect-profile" :
472529 payload = (
473530 build_periodic_report_preset_activation (args .preset )
474531 if args .preset
475532 else build_periodic_report_activation (
476- _load_json_object (args .profile_json )
533+ _load_json_object (args .profile_json , args . max_input_bytes )
477534 )
478535 )
479536 elif args .periodic_report_command == "evaluate-trigger" :
480- request = _load_json_object (args .request_json )
537+ request = _load_json_object (args .request_json , args . max_input_bytes )
481538 payload = build_periodic_report_trigger_decision (request )
482539 elif args .periodic_report_command == "evaluate-runtime-trigger" :
483- request = _load_json_object (args .request_json )
540+ request = _load_json_object (args .request_json , args . max_input_bytes )
484541 payload = build_periodic_report_runtime_trigger_decision (
485542 request ,
486543 rollout_events = iter_rollout_events (
@@ -550,7 +607,7 @@ def handle_periodic_report_command(
550607 if args .periodic_report_command == "configure-machine-defaults" :
551608 payload = configure_periodic_report_machine_defaults (
552609 runtime_root = runtime_root ,
553- machine_defaults = _load_json_object (args .config_json ),
610+ machine_defaults = _load_json_object (args .config_json , args . max_input_bytes ),
554611 execute = bool (args .execute ),
555612 expected_plan_revision = args .expected_plan_revision ,
556613 )
@@ -569,11 +626,11 @@ def handle_periodic_report_command(
569626 registry , runtime_root_arg , registry_path = registry_path
570627 )
571628 payload = build_goal_periodic_report_delivery_plan (
572- _load_json_object (args .request_json ),
629+ _load_json_object (args .request_json , args . max_input_bytes ),
573630 machine_defaults = read_periodic_report_machine_defaults (runtime_root ),
574631 )
575632 else :
576- request = _load_json_object (args .request_json )
633+ request = _load_json_object (args .request_json , args . max_input_bytes )
577634 payload = build_periodic_report_run (request )
578635 except PeriodicReportSubscriptionConfigurationError as exc :
579636 payload = {
0 commit comments