Skip to main content

stackforge_core/layer/ftp/
mod.rs

1//! FTP (File Transfer Protocol) layer implementation.
2//!
3//! Implements RFC 959 FTP packet parsing as a zero-copy view into a packet buffer.
4//!
5//! FTP operates over two TCP connections:
6//! - **Control connection** (port 21): Commands and replies (text-based)
7//! - **Data connection** (port 20 or negotiated): Actual file data
8//!
9//! This implementation focuses on the **control connection** protocol.
10//!
11//! ## Packet Format
12//!
13//! **Client Command:**
14//! ```text
15//! COMMAND [arguments]\r\n
16//! ```
17//!
18//! **Server Reply:**
19//! ```text
20//! NNN<SP>text\r\n               (single-line)
21//! NNN-text\r\n ... NNN<SP>text\r\n  (multi-line)
22//! ```
23//! Where NNN is a 3-digit reply code.
24//!
25//! ## Reply Code Categories
26//!
27//! | Range | Meaning                   |
28//! |-------|---------------------------|
29//! | 1xx   | Positive Preliminary      |
30//! | 2xx   | Positive Completion       |
31//! | 3xx   | Positive Intermediate     |
32//! | 4xx   | Transient Negative        |
33//! | 5xx   | Permanent Negative        |
34
35pub mod builder;
36pub use builder::FtpBuilder;
37
38use crate::layer::field::{FieldError, FieldValue};
39use crate::layer::{Layer, LayerIndex, LayerKind};
40
41/// Minimum FTP payload: at least "OK\r\n" or short command.
42pub const FTP_MIN_HEADER_LEN: usize = 4;
43
44/// FTP control port.
45pub const FTP_CONTROL_PORT: u16 = 21;
46
47/// FTP data port.
48pub const FTP_DATA_PORT: u16 = 20;
49
50// ============================================================================
51// FTP Reply code constants (RFC 959 ยง4.2)
52// ============================================================================
53pub const REPLY_RESTART_MARKER: u16 = 110;
54pub const REPLY_SERVICE_READY_IN: u16 = 120;
55pub const REPLY_DATA_OPEN_XFER: u16 = 125;
56pub const REPLY_FILE_STATUS_OK: u16 = 150;
57pub const REPLY_OK: u16 = 200;
58pub const REPLY_COMMAND_NOT_IMPLEMENTED: u16 = 202;
59pub const REPLY_SYSTEM_STATUS: u16 = 211;
60pub const REPLY_DIR_STATUS: u16 = 212;
61pub const REPLY_FILE_STATUS: u16 = 213;
62pub const REPLY_HELP_MSG: u16 = 214;
63pub const REPLY_NAME_SYSTEM: u16 = 215;
64pub const REPLY_SERVICE_READY: u16 = 220;
65pub const REPLY_CLOSING_CONTROL: u16 = 221;
66pub const REPLY_DATA_OPEN: u16 = 225;
67pub const REPLY_CLOSING_DATA: u16 = 226;
68pub const REPLY_PASSIVE: u16 = 227;
69pub const REPLY_LONG_PASSIVE: u16 = 228;
70pub const REPLY_EXTENDED_PASSIVE: u16 = 229;
71pub const REPLY_USER_LOGGED_IN: u16 = 230;
72pub const REPLY_AUTH_OK: u16 = 234;
73pub const REPLY_FILE_ACTION_OK: u16 = 250;
74pub const REPLY_PATHNAME_CREATED: u16 = 257;
75pub const REPLY_USER_OK_NEED_PASS: u16 = 331;
76pub const REPLY_NEED_ACCOUNT: u16 = 332;
77pub const REPLY_PENDING_INFO: u16 = 350;
78pub const REPLY_SERVICE_NOT_AVAIL: u16 = 421;
79pub const REPLY_CANT_OPEN_DATA: u16 = 425;
80pub const REPLY_CONN_CLOSED: u16 = 426;
81pub const REPLY_INVALID_CRED: u16 = 430;
82pub const REPLY_HOST_UNAVAIL: u16 = 434;
83pub const REPLY_FILE_UNAVAIL_BUSY: u16 = 450;
84pub const REPLY_LOCAL_ERROR: u16 = 451;
85pub const REPLY_INSUFF_STORAGE: u16 = 452;
86pub const REPLY_SYNTAX_ERROR: u16 = 500;
87pub const REPLY_ARG_SYNTAX_ERROR: u16 = 501;
88pub const REPLY_CMD_NOT_IMPL: u16 = 502;
89pub const REPLY_BAD_SEQUENCE: u16 = 503;
90pub const REPLY_CMD_NOT_IMPL_PARAM: u16 = 504;
91pub const REPLY_NOT_LOGGED_IN: u16 = 530;
92pub const REPLY_NEED_ACCOUNT_FOR_STOR: u16 = 532;
93pub const REPLY_FILE_UNAVAIL: u16 = 550;
94pub const REPLY_PAGE_TYPE_UNKNOWN: u16 = 551;
95pub const REPLY_EXCEED_STORAGE: u16 = 552;
96pub const REPLY_FILENAME_NOT_ALLOWED: u16 = 553;
97
98// ============================================================================
99// FTP command constants
100// ============================================================================
101pub const CMD_USER: &str = "USER";
102pub const CMD_PASS: &str = "PASS";
103pub const CMD_ACCT: &str = "ACCT";
104pub const CMD_CWD: &str = "CWD";
105pub const CMD_CDUP: &str = "CDUP";
106pub const CMD_SMNT: &str = "SMNT";
107pub const CMD_QUIT: &str = "QUIT";
108pub const CMD_REIN: &str = "REIN";
109pub const CMD_PORT: &str = "PORT";
110pub const CMD_PASV: &str = "PASV";
111pub const CMD_TYPE: &str = "TYPE";
112pub const CMD_STRU: &str = "STRU";
113pub const CMD_MODE: &str = "MODE";
114pub const CMD_RETR: &str = "RETR";
115pub const CMD_STOR: &str = "STOR";
116pub const CMD_STOU: &str = "STOU";
117pub const CMD_APPE: &str = "APPE";
118pub const CMD_ALLO: &str = "ALLO";
119pub const CMD_REST: &str = "REST";
120pub const CMD_RNFR: &str = "RNFR";
121pub const CMD_RNTO: &str = "RNTO";
122pub const CMD_ABOR: &str = "ABOR";
123pub const CMD_DELE: &str = "DELE";
124pub const CMD_RMD: &str = "RMD";
125pub const CMD_MKD: &str = "MKD";
126pub const CMD_PWD: &str = "PWD";
127pub const CMD_LIST: &str = "LIST";
128pub const CMD_NLST: &str = "NLST";
129pub const CMD_SITE: &str = "SITE";
130pub const CMD_SYST: &str = "SYST";
131pub const CMD_STAT: &str = "STAT";
132pub const CMD_HELP: &str = "HELP";
133pub const CMD_NOOP: &str = "NOOP";
134// Extensions (RFC 2389, RFC 3659, RFC 2428)
135pub const CMD_FEAT: &str = "FEAT";
136pub const CMD_OPTS: &str = "OPTS";
137pub const CMD_EPRT: &str = "EPRT";
138pub const CMD_EPSV: &str = "EPSV";
139pub const CMD_MDTM: &str = "MDTM";
140pub const CMD_SIZE: &str = "SIZE";
141pub const CMD_MLST: &str = "MLST";
142pub const CMD_MLSD: &str = "MLSD";
143pub const CMD_AUTH: &str = "AUTH";
144pub const CMD_PROT: &str = "PROT";
145pub const CMD_PBSZ: &str = "PBSZ";
146
147/// FTP command verbs for detection.
148pub static FTP_COMMANDS: &[&str] = &[
149    "USER", "PASS", "ACCT", "CWD", "CDUP", "SMNT", "QUIT", "REIN", "PORT", "PASV", "TYPE", "STRU",
150    "MODE", "RETR", "STOR", "STOU", "APPE", "ALLO", "REST", "RNFR", "RNTO", "ABOR", "DELE", "RMD",
151    "MKD", "PWD", "LIST", "NLST", "SITE", "SYST", "STAT", "HELP", "NOOP", "FEAT", "OPTS", "EPRT",
152    "EPSV", "MDTM", "SIZE", "MLST", "MLSD", "AUTH", "PROT", "PBSZ",
153];
154
155/// Field names for Python/generic access.
156pub static FTP_FIELD_NAMES: &[&str] = &[
157    "command",
158    "args",
159    "reply_code",
160    "reply_text",
161    "is_response",
162    "is_multiline",
163    "raw",
164];
165
166// ============================================================================
167// Payload detection
168// ============================================================================
169
170/// Returns true if `buf` looks like an FTP control-connection payload.
171///
172/// FTP is text-based. We check for either:
173/// - A 3-digit ASCII reply code followed by space, dash, or CR/LF
174/// - A recognized FTP command verb followed by space or CR/LF
175#[must_use]
176pub fn is_ftp_payload(buf: &[u8]) -> bool {
177    if buf.len() < 3 {
178        return false;
179    }
180    // Check for FTP reply (3-digit code)
181    if buf[0].is_ascii_digit() && buf[1].is_ascii_digit() && buf[2].is_ascii_digit() {
182        return buf.len() >= 4 && matches!(buf[3], b' ' | b'-' | b'\r' | b'\n');
183    }
184    // Check for FTP command
185    if let Ok(text) = std::str::from_utf8(buf) {
186        let upper = text.to_ascii_uppercase();
187        let first_word = upper.split_ascii_whitespace().next().unwrap_or("");
188        // Strip trailing \r\n from first word if applicable
189        let first_word = first_word.trim_end_matches(['\r', '\n']);
190        return FTP_COMMANDS.contains(&first_word);
191    }
192    false
193}
194
195/// Represents an FTP message type.
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum FtpMessageKind {
198    /// Client command (e.g., USER, PASS, LIST)
199    Command,
200    /// Server reply (e.g., 220 Service ready)
201    Reply,
202    /// Unknown (cannot determine direction)
203    Unknown,
204}
205
206// ============================================================================
207// FtpLayer - zero-copy view
208// ============================================================================
209
210/// A zero-copy view into an FTP layer within a packet buffer.
211///
212/// FTP is text-based, so all field access involves parsing ASCII text
213/// from the buffer slice on demand.
214#[must_use]
215#[derive(Debug, Clone)]
216pub struct FtpLayer {
217    pub index: LayerIndex,
218}
219
220impl FtpLayer {
221    pub fn new(index: LayerIndex) -> Self {
222        Self { index }
223    }
224
225    pub fn at_start(len: usize) -> Self {
226        Self {
227            index: LayerIndex::new(LayerKind::Ftp, 0, len),
228        }
229    }
230
231    /// Returns the raw bytes of this layer.
232    #[inline]
233    fn slice<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
234        let end = self.index.end.min(buf.len());
235        &buf[self.index.start..end]
236    }
237
238    /// Determine if this message is a reply (starts with 3-digit code) or command.
239    #[must_use]
240    pub fn message_kind(&self, buf: &[u8]) -> FtpMessageKind {
241        let s = self.slice(buf);
242        if s.len() >= 3 && s[0].is_ascii_digit() && s[1].is_ascii_digit() && s[2].is_ascii_digit() {
243            FtpMessageKind::Reply
244        } else if self.command(buf).is_ok() {
245            FtpMessageKind::Command
246        } else {
247            FtpMessageKind::Unknown
248        }
249    }
250
251    /// Returns the FTP command verb (for client messages).
252    ///
253    /// Returns `Err` if this is a server reply, not a command.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`FieldError::InvalidValue`] if the payload is not valid UTF-8.
258    pub fn command(&self, buf: &[u8]) -> Result<String, FieldError> {
259        let s = self.slice(buf);
260        let text = std::str::from_utf8(s)
261            .map_err(|_| FieldError::InvalidValue("non-UTF8 FTP payload".into()))?;
262        let first_word = text.split_ascii_whitespace().next().unwrap_or("");
263        let upper = first_word.to_ascii_uppercase();
264        // Verify it's an FTP command
265        if FTP_COMMANDS.contains(&upper.as_str()) {
266            Ok(upper)
267        } else if !s.is_empty() && s[0].is_ascii_digit() {
268            Err(FieldError::InvalidValue(
269                "this is a reply, not a command".into(),
270            ))
271        } else {
272            Ok(upper) // return whatever verb is there
273        }
274    }
275
276    /// Returns the command arguments (everything after the verb on the first line).
277    ///
278    /// # Errors
279    ///
280    /// Returns [`FieldError::InvalidValue`] if the payload is not valid UTF-8.
281    pub fn args(&self, buf: &[u8]) -> Result<String, FieldError> {
282        let s = self.slice(buf);
283        let text = std::str::from_utf8(s)
284            .map_err(|_| FieldError::InvalidValue("non-UTF8 FTP payload".into()))?;
285        let first_line = text.lines().next().unwrap_or("");
286        // Skip the first word (command verb)
287        let rest = first_line
288            .split_once(' ')
289            .map_or("", |(_, r)| r)
290            .trim_end_matches(['\r', '\n']);
291        Ok(rest.to_string())
292    }
293
294    /// Returns the 3-digit reply code (for server replies).
295    ///
296    /// # Errors
297    ///
298    /// Returns [`FieldError::BufferTooShort`] if fewer than 3 bytes are available,
299    /// or [`FieldError::InvalidValue`] if the first 3 bytes are not ASCII digits.
300    pub fn reply_code(&self, buf: &[u8]) -> Result<u16, FieldError> {
301        let s = self.slice(buf);
302        if s.len() < 3 {
303            return Err(FieldError::BufferTooShort {
304                offset: self.index.start,
305                need: 3,
306                have: s.len(),
307            });
308        }
309        if s[0].is_ascii_digit() && s[1].is_ascii_digit() && s[2].is_ascii_digit() {
310            let code =
311                u16::from(s[0] - b'0') * 100 + u16::from(s[1] - b'0') * 10 + u16::from(s[2] - b'0');
312            Ok(code)
313        } else {
314            Err(FieldError::InvalidValue(
315                "payload does not start with a 3-digit reply code".into(),
316            ))
317        }
318    }
319
320    /// Returns the reply text (text following the code, stripped of CR/LF).
321    ///
322    /// # Errors
323    ///
324    /// Returns [`FieldError::InvalidValue`] if the payload is not valid UTF-8 or
325    /// does not begin with a valid 3-digit reply code.
326    pub fn reply_text(&self, buf: &[u8]) -> Result<String, FieldError> {
327        let s = self.slice(buf);
328        let text = std::str::from_utf8(s)
329            .map_err(|_| FieldError::InvalidValue("non-UTF8 FTP payload".into()))?;
330        // Get first line, skip the code prefix (NNN SP or NNN-)
331        let first_line = text.lines().next().unwrap_or("");
332        if first_line.len() >= 4 {
333            let msg = first_line[4..].trim_end_matches(['\r', '\n']).to_string();
334            Ok(msg)
335        } else if first_line.len() == 3 {
336            Ok(String::new())
337        } else {
338            Err(FieldError::InvalidValue(
339                "payload too short for reply format".into(),
340            ))
341        }
342    }
343
344    /// Returns true if this is a server reply (starts with a 3-digit code).
345    #[must_use]
346    pub fn is_response(&self, buf: &[u8]) -> bool {
347        let s = self.slice(buf);
348        s.len() >= 3 && s[0].is_ascii_digit() && s[1].is_ascii_digit() && s[2].is_ascii_digit()
349    }
350
351    /// Returns true if this is a multi-line reply (code followed by `-`).
352    #[must_use]
353    pub fn is_multiline(&self, buf: &[u8]) -> bool {
354        let s = self.slice(buf);
355        s.len() >= 4
356            && s[0].is_ascii_digit()
357            && s[1].is_ascii_digit()
358            && s[2].is_ascii_digit()
359            && s[3] == b'-'
360    }
361
362    /// Returns the raw payload as a UTF-8 string (best effort).
363    #[must_use]
364    pub fn raw(&self, buf: &[u8]) -> String {
365        let s = self.slice(buf);
366        String::from_utf8_lossy(s).to_string()
367    }
368
369    /// Get a field by name.
370    pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
371        match name {
372            "command" => Some(self.command(buf).map(FieldValue::Str)),
373            "args" => Some(self.args(buf).map(FieldValue::Str)),
374            "reply_code" => Some(self.reply_code(buf).map(FieldValue::U16)),
375            "reply_text" => Some(self.reply_text(buf).map(FieldValue::Str)),
376            "is_response" => Some(Ok(FieldValue::Bool(self.is_response(buf)))),
377            "is_multiline" => Some(Ok(FieldValue::Bool(self.is_multiline(buf)))),
378            "raw" => Some(Ok(FieldValue::Str(self.raw(buf)))),
379            _ => None,
380        }
381    }
382}
383
384impl Layer for FtpLayer {
385    fn kind(&self) -> LayerKind {
386        LayerKind::Ftp
387    }
388
389    fn summary(&self, buf: &[u8]) -> String {
390        let s = self.slice(buf);
391        let text = String::from_utf8_lossy(s);
392        let first_line = text.lines().next().unwrap_or("").trim_end_matches('\r');
393        format!("FTP {first_line}")
394    }
395
396    fn header_len(&self, buf: &[u8]) -> usize {
397        self.slice(buf).len()
398    }
399
400    fn hashret(&self, buf: &[u8]) -> Vec<u8> {
401        // Use reply code or command verb as hash key
402        if let Ok(code) = self.reply_code(buf) {
403            code.to_be_bytes().to_vec()
404        } else if let Ok(cmd) = self.command(buf) {
405            cmd.into_bytes()
406        } else {
407            vec![]
408        }
409    }
410
411    fn field_names(&self) -> &'static [&'static str] {
412        FTP_FIELD_NAMES
413    }
414}
415
416/// Display fields for `FtpLayer` in `show()` output.
417#[must_use]
418pub fn ftp_show_fields(l: &FtpLayer, buf: &[u8]) -> Vec<(&'static str, String)> {
419    let mut fields = Vec::new();
420    if l.is_response(buf) {
421        if let Ok(code) = l.reply_code(buf) {
422            fields.push(("reply_code", code.to_string()));
423        }
424        if let Ok(text) = l.reply_text(buf) {
425            fields.push(("reply_text", text));
426        }
427        fields.push(("is_multiline", l.is_multiline(buf).to_string()));
428    } else if let Ok(cmd) = l.command(buf) {
429        fields.push(("command", cmd));
430        if let Ok(args) = l.args(buf)
431            && !args.is_empty()
432        {
433            fields.push(("args", args));
434        }
435    }
436    fields
437}
438
439// ============================================================================
440// Helper: FTP reply code description
441// ============================================================================
442
443/// Returns a human-readable description for an FTP reply code.
444#[must_use]
445pub fn reply_code_description(code: u16) -> &'static str {
446    match code {
447        110 => "Restart marker reply",
448        120 => "Service ready in N minutes",
449        125 => "Data connection already open; transfer starting",
450        150 => "File status okay; about to open data connection",
451        200 => "Command okay",
452        202 => "Command not implemented, superfluous at this site",
453        211 => "System status, or system help reply",
454        212 => "Directory status",
455        213 => "File status",
456        214 => "Help message",
457        215 => "NAME system type",
458        220 => "Service ready for new user",
459        221 => "Service closing control connection",
460        225 => "Data connection open; no transfer in progress",
461        226 => "Closing data connection; requested file action successful",
462        227 => "Entering Passive Mode",
463        228 => "Entering Long Passive Mode",
464        229 => "Entering Extended Passive Mode",
465        230 => "User logged in, proceed",
466        234 => "Specifying protection mechanism name",
467        250 => "Requested file action okay, completed",
468        257 => "PATHNAME created",
469        331 => "User name okay, need password",
470        332 => "Need account for login",
471        350 => "Requested file action pending further information",
472        421 => "Service not available, closing control connection",
473        425 => "Can't open data connection",
474        426 => "Connection closed; transfer aborted",
475        430 => "Invalid username or password",
476        434 => "Requested host unavailable",
477        450 => "Requested file action not taken; file unavailable",
478        451 => "Requested action aborted; local error in processing",
479        452 => "Requested action not taken; insufficient storage space",
480        500 => "Syntax error, command unrecognized",
481        501 => "Syntax error in parameters or arguments",
482        502 => "Command not implemented",
483        503 => "Bad sequence of commands",
484        504 => "Command not implemented for that parameter",
485        530 => "Not logged in",
486        532 => "Need account for storing files",
487        550 => "Requested action not taken; file unavailable",
488        551 => "Requested action aborted; page type unknown",
489        552 => "Requested file action aborted; exceeded storage allocation",
490        553 => "Requested action not taken; file name not allowed",
491        _ => "Unknown reply code",
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use crate::layer::LayerIndex;
499
500    fn make_layer(data: &[u8]) -> FtpLayer {
501        FtpLayer::new(LayerIndex::new(LayerKind::Ftp, 0, data.len()))
502    }
503
504    #[test]
505    fn test_ftp_detection_reply() {
506        assert!(is_ftp_payload(b"220 Service ready\r\n"));
507        assert!(is_ftp_payload(b"331 Password required\r\n"));
508        assert!(is_ftp_payload(b"230 User logged in\r\n"));
509        assert!(is_ftp_payload(b"550 File not found\r\n"));
510    }
511
512    #[test]
513    fn test_ftp_detection_command() {
514        assert!(is_ftp_payload(b"USER anonymous\r\n"));
515        assert!(is_ftp_payload(b"PASS secret\r\n"));
516        assert!(is_ftp_payload(b"LIST\r\n"));
517        assert!(is_ftp_payload(b"QUIT\r\n"));
518        assert!(is_ftp_payload(b"RETR file.txt\r\n"));
519    }
520
521    #[test]
522    fn test_ftp_detection_negative() {
523        assert!(!is_ftp_payload(b""));
524        assert!(!is_ftp_payload(b"GET / HTTP/1.1\r\n"));
525        assert!(!is_ftp_payload(b"\x00\x00\x00\x01"));
526    }
527
528    #[test]
529    fn test_ftp_layer_reply_code() {
530        let data = b"220 Service ready for new user\r\n";
531        let layer = make_layer(data);
532        assert_eq!(layer.reply_code(data).unwrap(), 220);
533        assert_eq!(
534            layer.reply_text(data).unwrap(),
535            "Service ready for new user"
536        );
537        assert!(layer.is_response(data));
538        assert!(!layer.is_multiline(data));
539    }
540
541    #[test]
542    fn test_ftp_layer_multiline_reply() {
543        let data = b"220-Welcome to FTP\r\n220 Ready\r\n";
544        let layer = make_layer(data);
545        assert_eq!(layer.reply_code(data).unwrap(), 220);
546        assert!(layer.is_multiline(data));
547    }
548
549    #[test]
550    fn test_ftp_layer_command() {
551        let data = b"USER anonymous\r\n";
552        let layer = make_layer(data);
553        assert_eq!(layer.command(data).unwrap(), "USER");
554        assert_eq!(layer.args(data).unwrap(), "anonymous");
555        assert!(!layer.is_response(data));
556        assert_eq!(layer.message_kind(data), FtpMessageKind::Command);
557    }
558
559    #[test]
560    fn test_ftp_layer_command_no_args() {
561        let data = b"QUIT\r\n";
562        let layer = make_layer(data);
563        assert_eq!(layer.command(data).unwrap(), "QUIT");
564        assert_eq!(layer.args(data).unwrap(), "");
565    }
566
567    #[test]
568    fn test_ftp_layer_pasv_response() {
569        let data = b"227 Entering Passive Mode (192,168,1,1,200,50)\r\n";
570        let layer = make_layer(data);
571        assert_eq!(layer.reply_code(data).unwrap(), 227);
572        assert!(layer.reply_text(data).unwrap().contains("Passive Mode"));
573    }
574
575    #[test]
576    fn test_ftp_reply_code_description() {
577        assert_eq!(reply_code_description(220), "Service ready for new user");
578        assert_eq!(reply_code_description(331), "User name okay, need password");
579        assert_eq!(
580            reply_code_description(550),
581            "Requested action not taken; file unavailable"
582        );
583    }
584
585    #[test]
586    fn test_ftp_field_access() {
587        let data = b"230 User logged in, proceed\r\n";
588        let layer = make_layer(data);
589        assert!(matches!(
590            layer.get_field(data, "reply_code"),
591            Some(Ok(FieldValue::U16(230)))
592        ));
593        assert!(matches!(
594            layer.get_field(data, "is_response"),
595            Some(Ok(FieldValue::Bool(true)))
596        ));
597        assert!(layer.get_field(data, "unknown_field").is_none());
598    }
599}