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}