Skip to main content

stackforge_core/layer/tftp/
mod.rs

1//! TFTP (Trivial File Transfer Protocol) layer implementation.
2//!
3//! Implements RFC 1350 TFTP packet parsing as a zero-copy view into a packet buffer.
4//!
5//! TFTP operates over UDP port 69 and defines 5 packet types:
6//!
7//! ## Packet Types
8//!
9//! | Opcode | Abbreviation | Packet Type       |
10//! |--------|--------------|-------------------|
11//! | 1      | RRQ          | Read Request      |
12//! | 2      | WRQ          | Write Request     |
13//! | 3      | DATA         | Data              |
14//! | 4      | ACK          | Acknowledgment    |
15//! | 5      | ERROR        | Error             |
16//!
17//! ## Packet Formats
18//!
19//! **RRQ / WRQ:**
20//! ```text
21//! 2 bytes   string   1 byte   string   1 byte
22//! +---------+--------+--------+--------+--------+
23//! | Opcode  |Filename|   0    |  Mode  |   0    |
24//! +---------+--------+--------+--------+--------+
25//! ```
26//!
27//! **DATA:**
28//! ```text
29//! 2 bytes   2 bytes   n bytes
30//! +---------+---------+--------+
31//! | Opcode  |  Block# |  Data  |
32//! +---------+---------+--------+
33//! ```
34//!
35//! **ACK:**
36//! ```text
37//! 2 bytes   2 bytes
38//! +---------+---------+
39//! | Opcode  |  Block# |
40//! +---------+---------+
41//! ```
42//!
43//! **ERROR:**
44//! ```text
45//! 2 bytes   2 bytes   string   1 byte
46//! +---------+---------+--------+--------+
47//! | Opcode  | ErrorCode| ErrMsg |   0    |
48//! +---------+---------+--------+--------+
49//! ```
50
51pub mod builder;
52pub use builder::TftpBuilder;
53
54use crate::layer::field::{FieldError, FieldValue};
55use crate::layer::{Layer, LayerIndex, LayerKind};
56
57/// Minimum TFTP header: opcode (2 bytes).
58pub const TFTP_MIN_HEADER_LEN: usize = 2;
59
60/// TFTP server UDP port.
61pub const TFTP_PORT: u16 = 69;
62
63/// Default TFTP data block size (bytes).
64pub const TFTP_DEFAULT_BLOCK_SIZE: usize = 512;
65
66// ============================================================================
67// Opcode constants (RFC 1350 §5)
68// ============================================================================
69pub const OPCODE_RRQ: u16 = 1;
70pub const OPCODE_WRQ: u16 = 2;
71pub const OPCODE_DATA: u16 = 3;
72pub const OPCODE_ACK: u16 = 4;
73pub const OPCODE_ERROR: u16 = 5;
74
75// ============================================================================
76// Error code constants (RFC 1350 §5)
77// ============================================================================
78pub const ERR_UNDEFINED: u16 = 0;
79pub const ERR_FILE_NOT_FOUND: u16 = 1;
80pub const ERR_ACCESS_VIOLATION: u16 = 2;
81pub const ERR_DISK_FULL: u16 = 3;
82pub const ERR_ILLEGAL_OPERATION: u16 = 4;
83pub const ERR_UNKNOWN_TID: u16 = 5;
84pub const ERR_FILE_EXISTS: u16 = 6;
85pub const ERR_NO_SUCH_USER: u16 = 7;
86
87/// Field names for Python/generic access.
88pub static TFTP_FIELD_NAMES: &[&str] = &[
89    "opcode",
90    "op_name",
91    "filename",
92    "mode",
93    "block_num",
94    "data",
95    "error_code",
96    "error_msg",
97];
98
99// ============================================================================
100// Payload detection
101// ============================================================================
102
103/// Returns true if `buf` looks like a TFTP payload.
104///
105/// A valid TFTP packet starts with a 2-byte opcode in range [1, 5].
106#[must_use]
107pub fn is_tftp_payload(buf: &[u8]) -> bool {
108    if buf.len() < 2 {
109        return false;
110    }
111    let opcode = u16::from_be_bytes([buf[0], buf[1]]);
112    (1..=5).contains(&opcode)
113}
114
115/// Returns a human-readable name for a TFTP opcode.
116#[must_use]
117pub fn opcode_name(opcode: u16) -> &'static str {
118    match opcode {
119        OPCODE_RRQ => "RRQ",
120        OPCODE_WRQ => "WRQ",
121        OPCODE_DATA => "DATA",
122        OPCODE_ACK => "ACK",
123        OPCODE_ERROR => "ERROR",
124        _ => "UNKNOWN",
125    }
126}
127
128/// Returns a human-readable description for a TFTP error code.
129#[must_use]
130pub fn error_code_description(code: u16) -> &'static str {
131    match code {
132        ERR_UNDEFINED => "Not defined",
133        ERR_FILE_NOT_FOUND => "File not found",
134        ERR_ACCESS_VIOLATION => "Access violation",
135        ERR_DISK_FULL => "Disk full or allocation exceeded",
136        ERR_ILLEGAL_OPERATION => "Illegal TFTP operation",
137        ERR_UNKNOWN_TID => "Unknown transfer ID",
138        ERR_FILE_EXISTS => "File already exists",
139        ERR_NO_SUCH_USER => "No such user",
140        _ => "Unknown error",
141    }
142}
143
144// ============================================================================
145// TftpLayer - zero-copy view
146// ============================================================================
147
148/// A zero-copy view into a TFTP layer within a packet buffer.
149#[must_use]
150#[derive(Debug, Clone)]
151pub struct TftpLayer {
152    pub index: LayerIndex,
153}
154
155impl TftpLayer {
156    pub fn new(index: LayerIndex) -> Self {
157        Self { index }
158    }
159
160    pub fn at_start(len: usize) -> Self {
161        Self {
162            index: LayerIndex::new(LayerKind::Tftp, 0, len),
163        }
164    }
165
166    #[inline]
167    fn slice<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
168        let end = self.index.end.min(buf.len());
169        &buf[self.index.start..end]
170    }
171
172    /// Returns the 2-byte opcode.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`FieldError::BufferTooShort`] if fewer than 2 bytes are available.
177    pub fn opcode(&self, buf: &[u8]) -> Result<u16, FieldError> {
178        let s = self.slice(buf);
179        if s.len() < 2 {
180            return Err(FieldError::BufferTooShort {
181                offset: self.index.start,
182                need: 2,
183                have: s.len(),
184            });
185        }
186        Ok(u16::from_be_bytes([s[0], s[1]]))
187    }
188
189    /// Returns the opcode name.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`FieldError::BufferTooShort`] if fewer than 2 bytes are available.
194    pub fn op_name(&self, buf: &[u8]) -> Result<String, FieldError> {
195        self.opcode(buf).map(|op| opcode_name(op).to_string())
196    }
197
198    /// Returns the filename from a RRQ or WRQ packet.
199    ///
200    /// The filename is a null-terminated string starting at byte 2.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`FieldError::InvalidValue`] if the opcode is not RRQ/WRQ or the
205    /// filename bytes are not valid UTF-8, or [`FieldError::BufferTooShort`] if
206    /// the buffer is too small.
207    pub fn filename(&self, buf: &[u8]) -> Result<String, FieldError> {
208        let s = self.slice(buf);
209        let opcode = self.opcode(buf)?;
210        if opcode != OPCODE_RRQ && opcode != OPCODE_WRQ {
211            return Err(FieldError::InvalidValue(
212                "filename only available in RRQ/WRQ packets".into(),
213            ));
214        }
215        if s.len() < 3 {
216            return Err(FieldError::BufferTooShort {
217                offset: self.index.start,
218                need: 3,
219                have: s.len(),
220            });
221        }
222        // Find null terminator
223        let start = 2;
224        let end = s[start..]
225            .iter()
226            .position(|&b| b == 0)
227            .map_or(s.len(), |p| start + p);
228        let name = std::str::from_utf8(&s[start..end])
229            .map_err(|_| FieldError::InvalidValue("invalid UTF-8 in filename".into()))?;
230        Ok(name.to_string())
231    }
232
233    /// Returns the mode string from a RRQ or WRQ packet ("netascii", "octet", "mail").
234    ///
235    /// # Errors
236    ///
237    /// Returns [`FieldError::InvalidValue`] if the opcode is not RRQ/WRQ or the
238    /// mode bytes are not valid UTF-8.
239    pub fn mode(&self, buf: &[u8]) -> Result<String, FieldError> {
240        let s = self.slice(buf);
241        let opcode = self.opcode(buf)?;
242        if opcode != OPCODE_RRQ && opcode != OPCODE_WRQ {
243            return Err(FieldError::InvalidValue(
244                "mode only available in RRQ/WRQ packets".into(),
245            ));
246        }
247        // Skip past opcode + filename + null
248        let mut offset = 2;
249        while offset < s.len() && s[offset] != 0 {
250            offset += 1;
251        }
252        offset += 1; // skip null terminator
253
254        let mode_start = offset;
255        let mode_end = s[mode_start..]
256            .iter()
257            .position(|&b| b == 0)
258            .map_or(s.len(), |p| mode_start + p);
259
260        let mode = std::str::from_utf8(&s[mode_start..mode_end])
261            .map_err(|_| FieldError::InvalidValue("invalid UTF-8 in mode".into()))?;
262        Ok(mode.to_ascii_lowercase())
263    }
264
265    /// Returns the block number from a DATA or ACK packet.
266    ///
267    /// # Errors
268    ///
269    /// Returns [`FieldError::InvalidValue`] if the opcode is not DATA/ACK, or
270    /// [`FieldError::BufferTooShort`] if fewer than 4 bytes are available.
271    pub fn block_num(&self, buf: &[u8]) -> Result<u16, FieldError> {
272        let s = self.slice(buf);
273        let opcode = self.opcode(buf)?;
274        if opcode != OPCODE_DATA && opcode != OPCODE_ACK {
275            return Err(FieldError::InvalidValue(
276                "block_num only available in DATA/ACK packets".into(),
277            ));
278        }
279        if s.len() < 4 {
280            return Err(FieldError::BufferTooShort {
281                offset: self.index.start,
282                need: 4,
283                have: s.len(),
284            });
285        }
286        Ok(u16::from_be_bytes([s[2], s[3]]))
287    }
288
289    /// Returns the data payload from a DATA packet.
290    ///
291    /// # Errors
292    ///
293    /// Returns [`FieldError::InvalidValue`] if the opcode is not DATA, or
294    /// [`FieldError::BufferTooShort`] if fewer than 4 bytes are available.
295    pub fn data(&self, buf: &[u8]) -> Result<Vec<u8>, FieldError> {
296        let s = self.slice(buf);
297        let opcode = self.opcode(buf)?;
298        if opcode != OPCODE_DATA {
299            return Err(FieldError::InvalidValue(
300                "data only available in DATA packets".into(),
301            ));
302        }
303        if s.len() < 4 {
304            return Err(FieldError::BufferTooShort {
305                offset: self.index.start,
306                need: 4,
307                have: s.len(),
308            });
309        }
310        Ok(s[4..].to_vec())
311    }
312
313    /// Returns the error code from an ERROR packet.
314    ///
315    /// # Errors
316    ///
317    /// Returns [`FieldError::InvalidValue`] if the opcode is not ERROR, or
318    /// [`FieldError::BufferTooShort`] if fewer than 4 bytes are available.
319    pub fn error_code(&self, buf: &[u8]) -> Result<u16, FieldError> {
320        let s = self.slice(buf);
321        let opcode = self.opcode(buf)?;
322        if opcode != OPCODE_ERROR {
323            return Err(FieldError::InvalidValue(
324                "error_code only available in ERROR packets".into(),
325            ));
326        }
327        if s.len() < 4 {
328            return Err(FieldError::BufferTooShort {
329                offset: self.index.start,
330                need: 4,
331                have: s.len(),
332            });
333        }
334        Ok(u16::from_be_bytes([s[2], s[3]]))
335    }
336
337    /// Returns the error message from an ERROR packet.
338    ///
339    /// # Errors
340    ///
341    /// Returns [`FieldError::InvalidValue`] if the opcode is not ERROR or the
342    /// message is not valid UTF-8, or [`FieldError::BufferTooShort`] if fewer
343    /// than 5 bytes are available.
344    pub fn error_msg(&self, buf: &[u8]) -> Result<String, FieldError> {
345        let s = self.slice(buf);
346        let opcode = self.opcode(buf)?;
347        if opcode != OPCODE_ERROR {
348            return Err(FieldError::InvalidValue(
349                "error_msg only available in ERROR packets".into(),
350            ));
351        }
352        if s.len() < 5 {
353            return Err(FieldError::BufferTooShort {
354                offset: self.index.start,
355                need: 5,
356                have: s.len(),
357            });
358        }
359        let msg_start = 4;
360        let msg_end = s[msg_start..]
361            .iter()
362            .position(|&b| b == 0)
363            .map_or(s.len(), |p| msg_start + p);
364        let msg = std::str::from_utf8(&s[msg_start..msg_end])
365            .map_err(|_| FieldError::InvalidValue("invalid UTF-8 in error message".into()))?;
366        Ok(msg.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            "opcode" => Some(self.opcode(buf).map(FieldValue::U16)),
373            "op_name" => Some(self.op_name(buf).map(FieldValue::Str)),
374            "filename" => Some(self.filename(buf).map(FieldValue::Str)),
375            "mode" => Some(self.mode(buf).map(FieldValue::Str)),
376            "block_num" => Some(self.block_num(buf).map(FieldValue::U16)),
377            "data" => Some(self.data(buf).map(FieldValue::Bytes)),
378            "error_code" => Some(self.error_code(buf).map(FieldValue::U16)),
379            "error_msg" => Some(self.error_msg(buf).map(FieldValue::Str)),
380            _ => None,
381        }
382    }
383}
384
385impl Layer for TftpLayer {
386    fn kind(&self) -> LayerKind {
387        LayerKind::Tftp
388    }
389
390    fn summary(&self, buf: &[u8]) -> String {
391        let s = self.slice(buf);
392        if s.len() < 2 {
393            return "TFTP [truncated]".to_string();
394        }
395        let opcode = u16::from_be_bytes([s[0], s[1]]);
396        match opcode {
397            OPCODE_RRQ => {
398                let fname = self.filename(buf).unwrap_or_default();
399                let mode = self.mode(buf).unwrap_or_default();
400                format!("TFTP Read Request File: {fname} Mode: {mode}")
401            },
402            OPCODE_WRQ => {
403                let fname = self.filename(buf).unwrap_or_default();
404                let mode = self.mode(buf).unwrap_or_default();
405                format!("TFTP Write Request File: {fname} Mode: {mode}")
406            },
407            OPCODE_DATA => {
408                let block = self.block_num(buf).unwrap_or(0);
409                let data_len = if s.len() >= 4 { s.len() - 4 } else { 0 };
410                format!("TFTP Data Block#{block} ({data_len} bytes)")
411            },
412            OPCODE_ACK => {
413                let block = self.block_num(buf).unwrap_or(0);
414                format!("TFTP Ack Block#{block}")
415            },
416            OPCODE_ERROR => {
417                let code = self.error_code(buf).unwrap_or(0);
418                let msg = self.error_msg(buf).unwrap_or_default();
419                format!("TFTP Error Code: {code} Message: {msg}")
420            },
421            _ => format!("TFTP [unknown opcode {opcode}]"),
422        }
423    }
424
425    fn header_len(&self, buf: &[u8]) -> usize {
426        let s = self.slice(buf);
427        if s.len() < 2 {
428            return s.len();
429        }
430        let opcode = u16::from_be_bytes([s[0], s[1]]);
431        match opcode {
432            OPCODE_RRQ | OPCODE_WRQ | OPCODE_ERROR => s.len(), // variable-length
433            OPCODE_DATA | OPCODE_ACK => 4.min(s.len()),        // opcode + block#, data is payload
434            _ => 2,
435        }
436    }
437
438    fn hashret(&self, buf: &[u8]) -> Vec<u8> {
439        if let Ok(block) = self.block_num(buf) {
440            block.to_be_bytes().to_vec()
441        } else if let Ok(op) = self.opcode(buf) {
442            op.to_be_bytes().to_vec()
443        } else {
444            vec![]
445        }
446    }
447
448    fn field_names(&self) -> &'static [&'static str] {
449        TFTP_FIELD_NAMES
450    }
451}
452
453/// Display fields for `TftpLayer` in `show()` output.
454#[must_use]
455pub fn tftp_show_fields(l: &TftpLayer, buf: &[u8]) -> Vec<(&'static str, String)> {
456    let mut fields = Vec::new();
457    if let Ok(op) = l.opcode(buf) {
458        fields.push(("opcode", op.to_string()));
459        fields.push(("op_name", opcode_name(op).to_string()));
460        match op {
461            OPCODE_RRQ | OPCODE_WRQ => {
462                if let Ok(f) = l.filename(buf) {
463                    fields.push(("filename", f));
464                }
465                if let Ok(m) = l.mode(buf) {
466                    fields.push(("mode", m));
467                }
468            },
469            OPCODE_DATA | OPCODE_ACK => {
470                if let Ok(b) = l.block_num(buf) {
471                    fields.push(("block_num", b.to_string()));
472                }
473            },
474            OPCODE_ERROR => {
475                if let Ok(c) = l.error_code(buf) {
476                    fields.push(("error_code", c.to_string()));
477                }
478                if let Ok(m) = l.error_msg(buf) {
479                    fields.push(("error_msg", m));
480                }
481            },
482            _ => {},
483        }
484    }
485    fields
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    fn make_layer(data: &[u8]) -> TftpLayer {
493        TftpLayer::new(LayerIndex::new(LayerKind::Tftp, 0, data.len()))
494    }
495
496    #[test]
497    fn test_tftp_detection() {
498        // RRQ
499        let rrq = b"\x00\x01file.txt\x00octet\x00";
500        assert!(is_tftp_payload(rrq));
501        // DATA
502        assert!(is_tftp_payload(b"\x00\x03\x00\x01hello"));
503        // ACK
504        assert!(is_tftp_payload(b"\x00\x04\x00\x01"));
505        // ERROR
506        assert!(is_tftp_payload(b"\x00\x05\x00\x01File not found\x00"));
507        // Invalid
508        assert!(!is_tftp_payload(b"\x00\x06")); // opcode 6
509        assert!(!is_tftp_payload(b"\x00")); // too short
510        assert!(!is_tftp_payload(b"")); // empty
511    }
512
513    #[test]
514    fn test_tftp_rrq_parsing() {
515        let data = b"\x00\x01file.txt\x00octet\x00";
516        let layer = make_layer(data);
517        assert_eq!(layer.opcode(data).unwrap(), OPCODE_RRQ);
518        assert_eq!(layer.op_name(data).unwrap(), "RRQ");
519        assert_eq!(layer.filename(data).unwrap(), "file.txt");
520        assert_eq!(layer.mode(data).unwrap(), "octet");
521    }
522
523    #[test]
524    fn test_tftp_wrq_parsing() {
525        let data = b"\x00\x02upload.bin\x00netascii\x00";
526        let layer = make_layer(data);
527        assert_eq!(layer.opcode(data).unwrap(), OPCODE_WRQ);
528        assert_eq!(layer.filename(data).unwrap(), "upload.bin");
529        assert_eq!(layer.mode(data).unwrap(), "netascii");
530    }
531
532    #[test]
533    fn test_tftp_data_parsing() {
534        let data = b"\x00\x03\x00\x01hello world data";
535        let layer = make_layer(data);
536        assert_eq!(layer.opcode(data).unwrap(), OPCODE_DATA);
537        assert_eq!(layer.block_num(data).unwrap(), 1);
538        assert_eq!(layer.data(data).unwrap(), b"hello world data");
539    }
540
541    #[test]
542    fn test_tftp_ack_parsing() {
543        let data = b"\x00\x04\x00\x05";
544        let layer = make_layer(data);
545        assert_eq!(layer.opcode(data).unwrap(), OPCODE_ACK);
546        assert_eq!(layer.block_num(data).unwrap(), 5);
547    }
548
549    #[test]
550    fn test_tftp_error_parsing() {
551        let data = b"\x00\x05\x00\x01File not found\x00";
552        let layer = make_layer(data);
553        assert_eq!(layer.opcode(data).unwrap(), OPCODE_ERROR);
554        assert_eq!(layer.error_code(data).unwrap(), ERR_FILE_NOT_FOUND);
555        assert_eq!(layer.error_msg(data).unwrap(), "File not found");
556    }
557
558    #[test]
559    fn test_tftp_error_code_descriptions() {
560        assert_eq!(error_code_description(ERR_FILE_NOT_FOUND), "File not found");
561        assert_eq!(
562            error_code_description(ERR_ACCESS_VIOLATION),
563            "Access violation"
564        );
565        assert_eq!(
566            error_code_description(ERR_DISK_FULL),
567            "Disk full or allocation exceeded"
568        );
569    }
570
571    #[test]
572    fn test_tftp_field_access() {
573        let data = b"\x00\x04\x00\x02";
574        let layer = make_layer(data);
575        assert!(matches!(
576            layer.get_field(data, "opcode"),
577            Some(Ok(FieldValue::U16(4)))
578        ));
579        assert!(matches!(
580            layer.get_field(data, "block_num"),
581            Some(Ok(FieldValue::U16(2)))
582        ));
583        assert!(layer.get_field(data, "bad_field").is_none());
584    }
585}