Skip to main content

netget/server/xmlrpc/
actions.rs

1//! XML-RPC protocol actions implementation
2//!
3//! This module implements the action system for XML-RPC.
4//! The LLM controls all XML-RPC responses through these actions, including:
5//! - Method execution responses (success values, faults)
6//! - Introspection (system.listMethods, system.methodHelp, system.methodSignature)
7//! - Extensions (nil values, i8/64-bit integers, system.multicall)
8
9use crate::llm::actions::{
10    protocol_trait::{ActionResult, Protocol, Server},
11    ActionDefinition, Parameter,
12};
13use crate::protocol::log_template::LogTemplate;
14use crate::protocol::{Event, EventType};
15use crate::state::app_state::AppState;
16use anyhow::{Context, Result};
17use serde_json::json;
18use std::sync::LazyLock;
19use tracing::debug;
20
21use super::{generate_fault, generate_success_response, MethodCall, XmlRpcValue};
22
23/// XML-RPC protocol action handler
24pub struct XmlRpcProtocol;
25
26impl XmlRpcProtocol {
27    pub fn new() -> Self {
28        Self
29    }
30
31    /// Read an integer the model may have written as a JSON string.
32    ///
33    /// Models routinely quote numbers (`"8"` for `8`), and refusing that turns
34    /// an otherwise correct answer into an XML-RPC fault the caller cannot act
35    /// on. The rest of the tree already coerces this way (usb-fido2's
36    /// `approval_id` takes a number or a numeric string), so accept it here
37    /// too. Anything that is not a whole number is still refused.
38    fn as_integer(value: &serde_json::Value, what: &str) -> Result<i64> {
39        value
40            .as_i64()
41            .or_else(|| value.as_str().and_then(|s| s.trim().parse::<i64>().ok()))
42            .with_context(|| {
43                format!(
44                    "value must be an integer for value_type '{}', got {}",
45                    what, value
46                )
47            })
48    }
49
50    /// Same for doubles: `"19.99"` is a number a model plausibly quotes.
51    fn as_double(value: &serde_json::Value) -> Result<f64> {
52        value
53            .as_f64()
54            .or_else(|| value.as_str().and_then(|s| s.trim().parse::<f64>().ok()))
55            .with_context(|| {
56                format!(
57                    "value must be a number for value_type 'double', got {}",
58                    value
59                )
60            })
61    }
62
63    /// Same for booleans: XML-RPC itself writes them as 0/1, and models mirror
64    /// that or quote `"true"`.
65    fn as_boolean(value: &serde_json::Value) -> Result<bool> {
66        if let Some(b) = value.as_bool() {
67            return Ok(b);
68        }
69        if let Some(i) = value.as_i64() {
70            return Ok(i != 0);
71        }
72        match value.as_str().map(|s| s.trim().to_ascii_lowercase()) {
73            Some(ref s) if s == "true" || s == "1" => Ok(true),
74            Some(ref s) if s == "false" || s == "0" => Ok(false),
75            _ => Err(anyhow::anyhow!(
76                "value must be a boolean for value_type 'boolean', got {}",
77                value
78            )),
79        }
80    }
81
82    fn execute_success_response(&self, action: serde_json::Value) -> Result<ActionResult> {
83        let value_type = action
84            .get("value_type")
85            .and_then(|v| v.as_str())
86            .context("Missing 'value_type' parameter")?;
87
88        let value = action.get("value").context("Missing 'value' parameter")?;
89
90        // Convert JSON value to XmlRpcValue
91        let xmlrpc_value = match value_type {
92            "int" | "i4" => {
93                // Range-checked, not cast: `as i32` silently wrapped, so 5000000000
94                // went on the wire as 705032704.
95                let i = Self::as_integer(value, value_type)?;
96                let i = i32::try_from(i).map_err(|_| {
97                    anyhow::anyhow!(
98                        "{} does not fit in an XML-RPC <int> (-2147483648..2147483647); use value_type 'i8'",
99                        i
100                    )
101                })?;
102                XmlRpcValue::Int(i)
103            }
104            "i8" => {
105                let i = Self::as_integer(value, "i8")?;
106                XmlRpcValue::I8(i)
107            }
108            "boolean" | "bool" => {
109                let b = Self::as_boolean(value)?;
110                XmlRpcValue::Boolean(b)
111            }
112            "string" => {
113                let s = value.as_str().context("Invalid string value")?;
114                XmlRpcValue::String(s.to_string())
115            }
116            "double" => {
117                let d = Self::as_double(value)?;
118                if !d.is_finite() {
119                    // NaN/inf render as "NaN"/"inf", which is not valid <double>.
120                    return Err(anyhow::anyhow!("double value must be finite"));
121                }
122                XmlRpcValue::Double(d)
123            }
124            "array" => {
125                let arr = value.as_array().context("Invalid array value")?;
126                let items: Vec<XmlRpcValue> = arr
127                    .iter()
128                    .map(|v| self.json_to_xmlrpc_value(v))
129                    .collect::<Result<Vec<_>>>()?;
130                XmlRpcValue::Array(items)
131            }
132            "struct" => {
133                let obj = value.as_object().context("Invalid struct value")?;
134                let members: Vec<(String, XmlRpcValue)> = obj
135                    .iter()
136                    .map(|(k, v)| Ok((k.clone(), self.json_to_xmlrpc_value(v)?)))
137                    .collect::<Result<Vec<_>>>()?;
138                XmlRpcValue::Struct(members)
139            }
140            "nil" | "null" => XmlRpcValue::Nil,
141            _ => return Err(anyhow::anyhow!("Unknown value_type: {}", value_type)),
142        };
143
144        let xml = generate_success_response(&xmlrpc_value);
145
146        debug!("XML-RPC success response generated ({} bytes)", xml.len());
147        Ok(ActionResult::Output(xml.as_bytes().to_vec()))
148    }
149
150    fn execute_fault_response(&self, action: serde_json::Value) -> Result<ActionResult> {
151        // `fault_code` is declared `required: true`, and a value that is present but not a
152        // whole number is an error rather than a silent -32603. It used to be
153        // `.and_then(|v| v.as_i64()).unwrap_or(-32603)`, so the quoted form models routinely
154        // produce — `"fault_code": "-32601"` — became "internal error" on the wire: the model
155        // said "no such method" and the caller was told netget had broken. `as_integer`
156        // accepts the quoted form for the same reason every other field here does.
157        // Absent stays -32603, which is the honest default for "the handler did not say".
158        let code_i64 = match action.get("fault_code") {
159            Some(value) => Self::as_integer(value, "fault_code")?,
160            None => -32603,
161        };
162        let code = i32::try_from(code_i64).map_err(|_| {
163            anyhow::anyhow!("fault_code {} does not fit in an XML-RPC <int>", code_i64)
164        })?;
165
166        let message = action
167            .get("fault_string")
168            .and_then(|v| v.as_str())
169            .context("Missing 'fault_string' parameter")?;
170
171        let xml = generate_fault(code, message);
172
173        debug!("XML-RPC fault response: {} - {}", code, message);
174        Ok(ActionResult::Output(xml.as_bytes().to_vec()))
175    }
176
177    fn execute_list_methods_response(&self, action: serde_json::Value) -> Result<ActionResult> {
178        let methods = action
179            .get("methods")
180            .and_then(|v| v.as_array())
181            .context("Missing 'methods' parameter")?;
182
183        // Non-string entries used to be dropped silently while the log still
184        // reported methods.len(), so the log disagreed with the wire.
185        let method_list: Vec<XmlRpcValue> = methods
186            .iter()
187            .map(|v| {
188                v.as_str()
189                    .map(|s| XmlRpcValue::String(s.to_string()))
190                    .context("every entry of 'methods' must be a method-name string")
191            })
192            .collect::<Result<Vec<_>>>()?;
193
194        let method_count = method_list.len();
195        let response_value = XmlRpcValue::Array(method_list);
196        let xml = generate_success_response(&response_value);
197
198        debug!(
199            "XML-RPC system.listMethods response ({} methods)",
200            method_count
201        );
202        Ok(ActionResult::Output(xml.as_bytes().to_vec()))
203    }
204
205    fn execute_method_help_response(&self, action: serde_json::Value) -> Result<ActionResult> {
206        let help_text = action
207            .get("help_text")
208            .and_then(|v| v.as_str())
209            .context("Missing 'help_text' parameter")?;
210
211        let response_value = XmlRpcValue::String(help_text.to_string());
212        let xml = generate_success_response(&response_value);
213
214        debug!("XML-RPC system.methodHelp response");
215        Ok(ActionResult::Output(xml.as_bytes().to_vec()))
216    }
217
218    fn execute_method_signature_response(&self, action: serde_json::Value) -> Result<ActionResult> {
219        let signatures = action
220            .get("signatures")
221            .and_then(|v| v.as_array())
222            .context("Missing 'signatures' parameter")?;
223
224        // Convert signatures array to XML-RPC array of arrays
225        // A flat list like ["int","int"] used to produce an empty result with no
226        // error, because every non-array entry was filtered out.
227        let sig_list: Vec<XmlRpcValue> = signatures
228            .iter()
229            .map(|v| {
230                let arr = v.as_array().context(
231                    "each entry of 'signatures' must itself be an array of type names, \
232                     starting with the return type",
233                )?;
234                let types: Vec<XmlRpcValue> = arr
235                    .iter()
236                    .map(|t| {
237                        t.as_str()
238                            .map(|s| XmlRpcValue::String(s.to_string()))
239                            .context("signature type names must be strings")
240                    })
241                    .collect::<Result<Vec<_>>>()?;
242                Ok(XmlRpcValue::Array(types))
243            })
244            .collect::<Result<Vec<_>>>()?;
245
246        let response_value = XmlRpcValue::Array(sig_list);
247        let xml = generate_success_response(&response_value);
248
249        debug!("XML-RPC system.methodSignature response");
250        Ok(ActionResult::Output(xml.as_bytes().to_vec()))
251    }
252
253    /// Helper: Convert JSON value to XmlRpcValue (auto-detect type)
254    fn json_to_xmlrpc_value(&self, value: &serde_json::Value) -> Result<XmlRpcValue> {
255        match value {
256            serde_json::Value::Null => Ok(XmlRpcValue::Nil),
257            serde_json::Value::Bool(b) => Ok(XmlRpcValue::Boolean(*b)),
258            serde_json::Value::Number(n) => {
259                if let Some(i) = n.as_i64() {
260                    if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
261                        Ok(XmlRpcValue::Int(i as i32))
262                    } else {
263                        Ok(XmlRpcValue::I8(i))
264                    }
265                } else if let Some(f) = n.as_f64() {
266                    Ok(XmlRpcValue::Double(f))
267                } else {
268                    Err(anyhow::anyhow!("Invalid number"))
269                }
270            }
271            serde_json::Value::String(s) => Ok(XmlRpcValue::String(s.clone())),
272            serde_json::Value::Array(arr) => {
273                let items: Vec<XmlRpcValue> = arr
274                    .iter()
275                    .map(|v| self.json_to_xmlrpc_value(v))
276                    .collect::<Result<Vec<_>>>()?;
277                Ok(XmlRpcValue::Array(items))
278            }
279            serde_json::Value::Object(obj) => {
280                let members: Vec<(String, XmlRpcValue)> = obj
281                    .iter()
282                    .map(|(k, v)| Ok((k.clone(), self.json_to_xmlrpc_value(v)?)))
283                    .collect::<Result<Vec<_>>>()?;
284                Ok(XmlRpcValue::Struct(members))
285            }
286        }
287    }
288}
289
290// Implement Protocol trait (common functionality)
291impl Protocol for XmlRpcProtocol {
292    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
293        // XML-RPC is purely request-response, no async actions needed
294        Vec::new()
295    }
296    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
297        vec![
298            success_response_action(),
299            fault_response_action(),
300            list_methods_response_action(),
301            method_help_response_action(),
302            method_signature_response_action(),
303        ]
304    }
305    fn protocol_name(&self) -> &'static str {
306        "XML-RPC"
307    }
308    fn get_event_types(&self) -> Vec<EventType> {
309        vec![XMLRPC_METHOD_CALL_EVENT.clone()]
310    }
311    fn stack_name(&self) -> &'static str {
312        "ETH>IP>TCP>HTTP>XML-RPC"
313    }
314    fn keywords(&self) -> Vec<&'static str> {
315        vec!["xmlrpc", "xml-rpc", "xml rpc"]
316    }
317    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
318        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
319
320        ProtocolMetadataV2::builder()
321            .state(DevelopmentState::Experimental)
322            .implementation("Hand-written XML-RPC codec over HTTP POST (quick-xml, hyper 1)")
323            .llm_control("Every method call: a typed return value or a fault code/string")
324            .e2e_testing("curl and mocked in-process HTTP")
325            .notes(
326                "Parameters reach the model with their XML-RPC types: <int>5</int> is the \
327                 number 5, not \"5\". <base64> is reported as a structural description, not \
328                 an encoded blob. Faults are XML-escaped, so a fault_string containing < or \
329                 & is still valid XML. system.multicall is not implemented. Any path is \
330                 accepted; there is no auth, no rate limiting, and non-POST returns a fault \
331                 body with HTTP 200 rather than 405. Request bodies are capped at 4 MiB and \
332                 value nesting at 64 levels.",
333            )
334            .max_inbound_bytes(crate::server::xmlrpc::MAX_REQUEST_BODY_BYTES)
335            .request_only(
336                "XML-RPC is HTTP request/response; a server cannot send a peer anything unprompted",
337            )
338            .build()
339    }
340    fn description(&self) -> &'static str {
341        "XML-RPC server"
342    }
343    fn example_prompt(&self) -> &'static str {
344        "Start an XML-RPC server on port 8080 with methods add(a,b) and greet(name)"
345    }
346    fn group_name(&self) -> &'static str {
347        "AI & API"
348    }
349    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
350        use crate::llm::actions::StartupExamples;
351
352        StartupExamples::new(
353            // LLM mode: instruction-based
354            json!({
355                "type": "open_server",
356                "port": 8080,
357                "base_stack": "xmlrpc",
358                "instruction": "XML-RPC server with add(a,b), subtract(a,b), and system.listMethods. Return proper fault codes for errors"
359            }),
360            // Script mode: event_handlers with script handler
361            json!({
362                "type": "open_server",
363                "port": 8080,
364                "base_stack": "xmlrpc",
365                "event_handlers": [{
366                    "event_pattern": "xmlrpc_method_call",
367                    "handler": {
368                        "type": "script",
369                        "language": "python",
370                        "code": "method = event.get('method_name', '')\nparams = event.get('params', [])\nif method == 'add' and len(params) >= 2:\n    action('xmlrpc_success_response', value_type='int', value=params[0] + params[1])\nelse:\n    action('xmlrpc_fault_response', fault_code=-32601, fault_string='Method not found')"
371                    }
372                }]
373            }),
374            // Static mode: event_handlers with static actions
375            json!({
376                "type": "open_server",
377                "port": 8080,
378                "base_stack": "xmlrpc",
379                "event_handlers": [{
380                    "event_pattern": "xmlrpc_method_call",
381                    "handler": {
382                        "type": "static",
383                        "actions": [{
384                            "type": "xmlrpc_success_response",
385                            "value_type": "string",
386                            "value": "Hello from XML-RPC server"
387                        }]
388                    }
389                }]
390            }),
391        )
392    }
393}
394
395// Implement Server trait (server-specific functionality)
396impl Server for XmlRpcProtocol {
397    fn spawn(
398        &self,
399        ctx: crate::protocol::SpawnContext,
400    ) -> std::pin::Pin<
401        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
402    > {
403        Box::pin(async move {
404            use crate::server::xmlrpc::XmlRpcServer;
405            XmlRpcServer::spawn_with_llm_actions(
406                ctx.legacy_listen_addr(),
407                ctx.llm_client,
408                ctx.state,
409                ctx.status_tx,
410                ctx.server_id,
411            )
412            .await
413        })
414    }
415    fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
416        let action_type = action
417            .get("type")
418            .and_then(|v| v.as_str())
419            .context("Missing 'type' field")?;
420
421        match action_type {
422            "xmlrpc_success_response" => self.execute_success_response(action),
423            "xmlrpc_fault_response" => self.execute_fault_response(action),
424            "xmlrpc_list_methods_response" => self.execute_list_methods_response(action),
425            "xmlrpc_method_help_response" => self.execute_method_help_response(action),
426            "xmlrpc_method_signature_response" => self.execute_method_signature_response(action),
427            _ => Err(anyhow::anyhow!("Unknown action type: {}", action_type)),
428        }
429    }
430}
431
432// Action definitions
433
434fn success_response_action() -> ActionDefinition {
435    ActionDefinition {
436        name: "xmlrpc_success_response".to_string(),
437        description: "Send XML-RPC success response with a value".to_string(),
438        parameters: vec![
439            Parameter {
440                name: "value_type".to_string(),
441                type_hint: "string".to_string(),
442                description: "Type of the return value (int, i8, boolean, string, double, array, struct, nil)".to_string(),
443                required: true,
444            },
445            Parameter {
446                name: "value".to_string(),
447                type_hint: "any".to_string(),
448                description: "The actual value to return (type must match value_type)".to_string(),
449                required: true,
450            },
451        ],
452        example: json!({
453            "type": "xmlrpc_success_response",
454            "value_type": "int",
455            "value": 42
456        }),
457        log_template: Some(
458            LogTemplate::new()
459                .with_info("-> XML-RPC success type={value_type}")
460                .with_debug("XML-RPC xmlrpc_success_response: type={value_type}"),
461        ),
462    }
463}
464
465fn fault_response_action() -> ActionDefinition {
466    ActionDefinition {
467        name: "xmlrpc_fault_response".to_string(),
468        description: "Send XML-RPC fault/error response".to_string(),
469        parameters: vec![
470            Parameter {
471                name: "fault_code".to_string(),
472                type_hint: "number".to_string(),
473                description: "Fault code (standard codes: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error)".to_string(),
474                required: true,
475            },
476            Parameter {
477                name: "fault_string".to_string(),
478                type_hint: "string".to_string(),
479                description: "Human-readable error message".to_string(),
480                required: true,
481            },
482        ],
483        example: json!({
484            "type": "xmlrpc_fault_response",
485            "fault_code": -32601,
486            "fault_string": "Method not found"
487        }),
488        log_template: Some(
489            LogTemplate::new()
490                .with_info("-> XML-RPC fault {fault_code}: {fault_string}")
491                .with_debug("XML-RPC xmlrpc_fault_response: code={fault_code} message={fault_string}"),
492        ),
493    }
494}
495
496fn list_methods_response_action() -> ActionDefinition {
497    ActionDefinition {
498        name: "xmlrpc_list_methods_response".to_string(),
499        description: "Respond to system.listMethods introspection with array of method names"
500            .to_string(),
501        parameters: vec![Parameter {
502            name: "methods".to_string(),
503            type_hint: "array".to_string(),
504            description: "Array of available method names (strings)".to_string(),
505            required: true,
506        }],
507        example: json!({
508            "type": "xmlrpc_list_methods_response",
509            "methods": ["add", "subtract", "system.listMethods", "system.methodHelp"]
510        }),
511        log_template: Some(
512            LogTemplate::new()
513                .with_info("-> XML-RPC list methods ({methods_len})")
514                .with_debug("XML-RPC xmlrpc_list_methods_response: count={methods_len}"),
515        ),
516    }
517}
518
519fn method_help_response_action() -> ActionDefinition {
520    ActionDefinition {
521        name: "xmlrpc_method_help_response".to_string(),
522        description: "Respond to system.methodHelp introspection with documentation string"
523            .to_string(),
524        parameters: vec![Parameter {
525            name: "help_text".to_string(),
526            type_hint: "string".to_string(),
527            description: "Documentation/help text for the requested method".to_string(),
528            required: true,
529        }],
530        example: json!({
531            "type": "xmlrpc_method_help_response",
532            "help_text": "add(a, b) - Returns the sum of two numbers"
533        }),
534        log_template: Some(
535            LogTemplate::new()
536                .with_info("-> XML-RPC method help")
537                .with_debug("XML-RPC xmlrpc_method_help_response"),
538        ),
539    }
540}
541
542fn method_signature_response_action() -> ActionDefinition {
543    ActionDefinition {
544        name: "xmlrpc_method_signature_response".to_string(),
545        description: "Respond to system.methodSignature introspection with array of signatures".to_string(),
546        parameters: vec![Parameter {
547            name: "signatures".to_string(),
548            type_hint: "array".to_string(),
549            description: "Array of signature arrays. Each signature is [return_type, param1_type, param2_type, ...]. Multiple signatures indicate overloads.".to_string(),
550            required: true,
551        }],
552        example: json!({
553            "type": "xmlrpc_method_signature_response",
554            "signatures": [["int", "int", "int"]]
555        }),
556        log_template: Some(
557            LogTemplate::new()
558                .with_info("-> XML-RPC method signature")
559                .with_debug("XML-RPC xmlrpc_method_signature_response: {signatures_len} sigs"),
560        ),
561    }
562}
563
564// Event type definitions
565
566/// XML-RPC method call event
567pub static XMLRPC_METHOD_CALL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
568    EventType::new(
569        "xmlrpc_method_call",
570        "XML-RPC method call received from client",
571        json!({
572            "type": "xmlrpc_success_response",
573            "value_type": "int",
574            "value": 42
575        }),
576    )
577    .with_parameters(vec![
578        Parameter {
579            name: "method_name".to_string(),
580            type_hint: "string".to_string(),
581            description: "Name of the RPC method being called".to_string(),
582            required: true,
583        },
584        Parameter {
585            name: "params".to_string(),
586            type_hint: "array".to_string(),
587            description: "Array of parameter values (can be integers, strings, booleans, arrays, structs, etc.)".to_string(),
588            required: true,
589        },
590    ])
591    .with_actions(vec![
592        success_response_action(),
593        fault_response_action(),
594        list_methods_response_action(),
595        method_help_response_action(),
596        method_signature_response_action(),
597    ])
598    .with_log_template(
599        LogTemplate::new()
600            .with_info("XML-RPC {method_name}")
601            .with_debug("XML-RPC method={method_name}")
602            .with_trace("XML-RPC: {json_pretty(.)}"),
603    )
604});
605
606/// Create event from method call
607pub fn create_method_call_event(method_call: &MethodCall) -> Event {
608    // Convert XmlRpcValue params to JSON
609    let params_json: Vec<serde_json::Value> = method_call
610        .params
611        .iter()
612        .map(xmlrpc_value_to_json)
613        .collect();
614
615    Event::new(
616        &XMLRPC_METHOD_CALL_EVENT,
617        json!({
618            "method_name": method_call.method_name,
619            "params": params_json,
620        }),
621    )
622}
623
624/// Convert XmlRpcValue to JSON for LLM
625fn xmlrpc_value_to_json(value: &XmlRpcValue) -> serde_json::Value {
626    match value {
627        XmlRpcValue::Int(i) => json!(i),
628        XmlRpcValue::I8(i) => json!(i),
629        XmlRpcValue::Boolean(b) => json!(b),
630        XmlRpcValue::String(s) => json!(s),
631        XmlRpcValue::Double(d) => json!(d),
632        XmlRpcValue::DateTime(dt) => json!(dt),
633        // Never hand the model a base64 blob (project rule: no raw bytes or
634        // encoded payloads in event data). Report it structurally, with the text
635        // only when the bytes happen to be UTF-8. This path is newly reachable:
636        // before the parser was fixed, <base64> was decoded as a plain string.
637        XmlRpcValue::Base64(bytes) => json!({
638            "xmlrpc_type": "base64",
639            "byte_length": bytes.len(),
640            "text": std::str::from_utf8(bytes).ok(),
641        }),
642        XmlRpcValue::Array(arr) => {
643            let items: Vec<serde_json::Value> = arr.iter().map(xmlrpc_value_to_json).collect();
644            json!(items)
645        }
646        XmlRpcValue::Struct(members) => {
647            let obj: serde_json::Map<String, serde_json::Value> = members
648                .iter()
649                .map(|(k, v)| (k.clone(), xmlrpc_value_to_json(v)))
650                .collect();
651            json!(obj)
652        }
653        XmlRpcValue::Nil => json!(null),
654    }
655}