Skip to main content

aria2_core/session/
session_serialize_impl.rs

1//! SessionEntry serialization/deserialization implementation
2//!
3//! This module provides the concrete implementation of [`SessionEntry::serialize()`]
4//! and [`SessionEntry::deserialize_line()`] methods.
5//!
6//! # Architecture
7//!
8//! ```text
9//! session_serialize_impl.rs (this file)
10//!   ├── impl SessionEntry { serialize(), deserialize_line() }
11//!   └── delegates to session_uri_utils for URI/hex handling
12//!
13//! session_entry.rs (core data)
14//!   ├── SessionEntry struct definition
15//!   └── Builder pattern methods (new, with_options, paused)
16//! ```
17//!
18//! # Serialization Format
19//!
20//! Each entry is serialized as a multi-line block:
21//! ```text
22//! uri1\turi2\turi3
23//!  GID=hex_value
24//!  [PAUSE=true]
25//!  key=value
26//!  TOTAL_LENGTH=...
27//!  COMPLETED_LENGTH=...
28//!  ...
29//! ```
30
31use crate::error::Result;
32use crate::session::session_entry::{SessionEntry, decode_hex, escape_uri, unescape_uri};
33
34impl SessionEntry {
35    /// Serializes this entry to the session file format
36    ///
37    /// Produces a multi-line string representation suitable for writing
38    /// to a session file. The format is compatible with aria2's session format.
39    ///
40    /// # Returns
41    ///
42    /// String containing the serialized entry (including trailing newline)
43    ///
44    /// # Format
45    ///
46    /// ```text
47    /// uri1\turi2\turi3
48    ///  GID=hex_value
49    ///  [PAUSE=true]
50    ///  option_key=option_value
51    ///  TOTAL_LENGTH=...
52    ///  COMPLETED_LENGTH=...
53    ///  ...
54    /// ```
55    ///
56    /// # Example
57    ///
58    /// ```rust
59    /// use aria2_core::session::session_entry::SessionEntry;
60    ///
61    /// let entry = SessionEntry::new(0xd270c8a2, vec!["http://example.com/file.zip".to_string()]);
62    /// let text = entry.serialize();
63    /// assert!(text.contains("http://example.com/file.zip"));
64    /// assert!(text.contains("GID=d270c8a2"));
65    /// ```
66    pub fn serialize(&self) -> String {
67        let mut lines = String::new();
68
69        // Serialize URIs (tab-separated, escaped)
70        let escaped_uris: Vec<String> = self.uris.iter().map(|u| escape_uri(u)).collect();
71        lines.push_str(&escaped_uris.join("\t"));
72        lines.push('\n');
73
74        // GID (always present, hex format)
75        lines.push_str(&format!(" GID={:x}\n", self.gid));
76
77        // PAUSE flag (only if true)
78        if self.paused {
79            lines.push_str(" PAUSE=true\n");
80        }
81
82        // User-defined options
83        for (key, value) in &self.options {
84            lines.push_str(&format!(" {}={}\n", key, value));
85        }
86
87        // Progress & status fields
88        lines.push_str(&format!(" TOTAL_LENGTH={}\n", self.total_length));
89        lines.push_str(&format!(" COMPLETED_LENGTH={}\n", self.completed_length));
90        lines.push_str(&format!(" UPLOAD_LENGTH={}\n", self.upload_length));
91        lines.push_str(&format!(" DOWNLOAD_SPEED={}\n", self.download_speed));
92        lines.push_str(&format!(" STATUS={}\n", self.status));
93
94        // ERROR_CODE (optional field)
95        match self.error_code {
96            Some(code) => lines.push_str(&format!(" ERROR_CODE={}\n", code)),
97            None => lines.push_str(" ERROR_CODE=\n"),
98        }
99
100        // BITFIELD (hex encoded or empty)
101        match &self.bitfield {
102            Some(bytes) => {
103                let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
104                lines.push_str(&format!(" BITFIELD={}\n", hex));
105            }
106            None => lines.push_str(" BITFIELD=\n"),
107        }
108
109        // NUM_PIECES and PIECE_LENGTH (BT-specific)
110        lines.push_str(&format!(" NUM_PIECES={}\n", self.num_pieces.unwrap_or(0)));
111        lines.push_str(&format!(
112            " PIECE_LENGTH={}\n",
113            self.piece_length.unwrap_or(0)
114        ));
115
116        // INFO_HASH (optional, BT-specific)
117        match &self.info_hash_hex {
118            Some(hash) => lines.push_str(&format!(" INFO_HASH={}\n", hash)),
119            None => lines.push_str(" INFO_HASH=\n"),
120        }
121
122        // RESUME_OFFSET (optional, HTTP/FTP resume support)
123        lines.push_str(&format!(
124            " RESUME_OFFSET={}\n",
125            self.resume_offset.unwrap_or(0)
126        ));
127
128        lines
129    }
130
131    /// Deserializes a single line of text into a SessionEntry
132    ///
133    /// Parses a text block containing URI lines and property lines
134    /// (lines starting with space) into a complete SessionEntry.
135    ///
136    /// # Arguments
137    ///
138    /// * `text` - Multi-line string containing one entry's data
139    ///
140    /// # Returns
141    ///
142    /// Result containing the deserialized SessionEntry or an error
143    ///
144    /// # Note
145    ///
146    /// This is a lower-level parsing function. For parsing multiple entries
147    /// from a full session file, use [`crate::session::session_serializer::deserialize()`].
148    pub fn deserialize_line(text: &str) -> Result<SessionEntry> {
149        let mut entry = SessionEntry::new(0, Vec::new());
150
151        for raw_line in text.lines() {
152            let line = raw_line.trim_end();
153
154            // Skip empty lines and comments within an entry
155            if line.is_empty() || line.starts_with('#') {
156                continue;
157            }
158
159            // Property lines must start with space
160            if let Some(rest) = line.strip_prefix(' ') {
161                let rest_trimmed = rest.trim();
162                if let Some((key, value)) = rest_trimmed.split_once('=') {
163                    Self::parse_property(key, value, &mut entry);
164                }
165                continue;
166            }
167
168            // This must be the URI line (first line without leading space)
169            let unescaped = unescape_uri(line.trim());
170            let uris: Vec<String> = unescaped
171                .split('\t')
172                .map(|s| s.to_string())
173                .filter(|s| !s.is_empty())
174                .collect();
175            if !uris.is_empty() {
176                entry.uris = uris;
177            }
178        }
179
180        Ok(entry)
181    }
182
183    /// Parse a single property line (KEY=VALUE) and update the entry
184    fn parse_property(key: &str, value: &str, entry: &mut SessionEntry) {
185        match key {
186            "GID" => {
187                if let Ok(gid) = u64::from_str_radix(value, 16) {
188                    entry.gid = gid;
189                }
190            }
191            "PAUSE" => {
192                if value == "true" {
193                    entry.paused = true;
194                }
195            }
196            // Progress & status fields
197            "TOTAL_LENGTH" => {
198                if let Ok(v) = value.parse::<u64>() {
199                    entry.total_length = v;
200                }
201            }
202            "COMPLETED_LENGTH" => {
203                if let Ok(v) = value.parse::<u64>() {
204                    entry.completed_length = v;
205                }
206            }
207            "UPLOAD_LENGTH" => {
208                if let Ok(v) = value.parse::<u64>() {
209                    entry.upload_length = v;
210                }
211            }
212            "DOWNLOAD_SPEED" => {
213                if let Ok(v) = value.parse::<u64>() {
214                    entry.download_speed = v;
215                }
216            }
217            "STATUS" => {
218                if !value.is_empty() {
219                    entry.status = value.to_string();
220                }
221            }
222            "ERROR_CODE" => {
223                if !value.is_empty()
224                    && let Ok(code) = value.parse::<i32>()
225                {
226                    entry.error_code = Some(code);
227                }
228            }
229            "BITFIELD" => {
230                if !value.is_empty() {
231                    if let Ok(bytes) = decode_hex(value) {
232                        entry.bitfield = Some(bytes);
233                    } else {
234                        tracing::warn!("Invalid BITFIELD hex string, ignoring");
235                    }
236                }
237            }
238            "NUM_PIECES" => {
239                if let Ok(v) = value.parse::<u32>()
240                    && v > 0
241                {
242                    entry.num_pieces = Some(v);
243                }
244            }
245            "PIECE_LENGTH" => {
246                if let Ok(v) = value.parse::<u32>()
247                    && v > 0
248                {
249                    entry.piece_length = Some(v);
250                }
251            }
252            "INFO_HASH" => {
253                if !value.is_empty() {
254                    entry.info_hash_hex = Some(value.to_string());
255                }
256            }
257            "RESUME_OFFSET" => {
258                if let Ok(v) = value.parse::<u64>()
259                    && v > 0
260                {
261                    entry.resume_offset = Some(v);
262                }
263            }
264            // User-defined options (not progress fields)
265            _ => {
266                entry.options.insert(key.to_string(), value.to_string());
267            }
268        }
269    }
270}