1use std::fmt;
8use std::sync::Arc;
9
10use crate::swarm::{BatchId, PublicKey, Reference};
11
12#[derive(Debug, Clone, Copy)]
16pub struct UploadProgress<'a> {
17 pub path: &'a str,
19 pub size: u64,
21 pub index: usize,
23 pub total: usize,
25}
26
27pub type OnEntryFn = Arc<dyn for<'a> Fn(UploadProgress<'a>) + Send + Sync>;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33#[repr(u8)]
34pub enum RedundancyLevel {
35 Off = 0,
37 Medium = 1,
39 Strong = 2,
41 Insane = 3,
43 Paranoid = 4,
45}
46
47impl RedundancyLevel {
48 pub fn as_u8(self) -> u8 {
50 self as u8
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57#[repr(u8)]
58pub enum RedundancyStrategy {
59 None = 0,
61 Data = 1,
63 Proximity = 2,
65 Race = 3,
67}
68
69impl RedundancyStrategy {
70 pub fn as_u8(self) -> u8 {
72 self as u8
73 }
74}
75
76#[derive(Clone, Debug, Default)]
79pub struct UploadOptions {
80 pub act: Option<bool>,
84 pub act_history_address: Option<Reference>,
87 pub pin: Option<bool>,
89 pub encrypt: Option<bool>,
92 pub tag: u32,
95 pub deferred: Option<bool>,
99}
100
101#[derive(Clone, Debug, Default)]
104pub struct RedundantUploadOptions {
105 pub base: UploadOptions,
107 pub redundancy_level: Option<RedundancyLevel>,
109}
110
111#[derive(Clone, Debug, Default)]
114pub struct FileUploadOptions {
115 pub base: UploadOptions,
117 pub size: Option<u64>,
120 pub content_type: Option<String>,
122 pub redundancy_level: Option<RedundancyLevel>,
124}
125
126#[derive(Clone, Default)]
129pub struct CollectionUploadOptions {
130 pub base: UploadOptions,
132 pub index_document: Option<String>,
134 pub error_document: Option<String>,
136 pub redundancy_level: Option<RedundancyLevel>,
138 pub on_entry: Option<OnEntryFn>,
142}
143
144impl CollectionUploadOptions {
145 pub fn with_on_entry<F>(mut self, f: F) -> Self
149 where
150 F: for<'a> Fn(UploadProgress<'a>) + Send + Sync + 'static,
151 {
152 self.on_entry = Some(Arc::new(f));
153 self
154 }
155}
156
157impl fmt::Debug for CollectionUploadOptions {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.debug_struct("CollectionUploadOptions")
160 .field("base", &self.base)
161 .field("index_document", &self.index_document)
162 .field("error_document", &self.error_document)
163 .field("redundancy_level", &self.redundancy_level)
164 .field("on_entry", &self.on_entry.as_ref().map(|_| "<callback>"))
165 .finish()
166 }
167}
168
169#[derive(Clone, Debug, Default)]
172pub struct DownloadOptions {
173 pub redundancy_strategy: Option<RedundancyStrategy>,
175 pub fallback: Option<bool>,
177 pub timeout_ms: Option<u64>,
179 pub act_publisher: Option<PublicKey>,
181 pub act_history_address: Option<Reference>,
183 pub act_timestamp: Option<i64>,
185}
186
187#[derive(Clone, Debug, Default)]
190pub struct PostageBatchOptions {
191 pub label: Option<String>,
193 pub immutable: Option<bool>,
195 pub gas_price: Option<String>,
197 pub gas_limit: Option<String>,
199}
200
201pub type HeaderPairs = Vec<(&'static str, String)>;
206
207fn bool_str(b: bool) -> &'static str {
208 if b { "true" } else { "false" }
209}
210
211fn push_upload_options(out: &mut HeaderPairs, opts: &UploadOptions) {
212 if let Some(v) = opts.pin {
213 out.push(("Swarm-Pin", bool_str(v).to_string()));
214 }
215 if let Some(v) = opts.encrypt {
216 out.push(("Swarm-Encrypt", bool_str(v).to_string()));
217 }
218 if opts.tag > 0 {
219 out.push(("Swarm-Tag", opts.tag.to_string()));
220 }
221 if let Some(v) = opts.deferred {
222 out.push(("Swarm-Deferred-Upload", bool_str(v).to_string()));
223 }
224 if let Some(v) = opts.act {
225 out.push(("Swarm-Act", bool_str(v).to_string()));
226 }
227 if let Some(ref r) = opts.act_history_address {
228 out.push(("Swarm-Act-History-Address", r.to_hex()));
229 }
230}
231
232pub fn prepare_upload_headers(batch_id: &BatchId, opts: Option<&UploadOptions>) -> HeaderPairs {
234 let mut out = vec![("Swarm-Postage-Batch-Id", batch_id.to_hex())];
235 if let Some(o) = opts {
236 push_upload_options(&mut out, o);
237 }
238 out
239}
240
241pub fn prepare_redundant_upload_headers(
243 batch_id: &BatchId,
244 opts: Option<&RedundantUploadOptions>,
245) -> HeaderPairs {
246 match opts {
247 None => prepare_upload_headers(batch_id, None),
248 Some(o) => {
249 let mut out = prepare_upload_headers(batch_id, Some(&o.base));
250 if let Some(level) = o.redundancy_level {
251 if !matches!(level, RedundancyLevel::Off) {
252 out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
253 }
254 }
255 out
256 }
257 }
258}
259
260pub fn prepare_file_upload_headers(
262 batch_id: &BatchId,
263 opts: Option<&FileUploadOptions>,
264) -> HeaderPairs {
265 match opts {
266 None => prepare_upload_headers(batch_id, None),
267 Some(o) => {
268 let mut out = prepare_upload_headers(batch_id, Some(&o.base));
269 if let Some(size) = o.size {
270 out.push(("Content-Length", size.to_string()));
271 }
272 if let Some(ref ct) = o.content_type {
273 out.push(("Content-Type", ct.clone()));
274 }
275 if let Some(level) = o.redundancy_level {
276 if !matches!(level, RedundancyLevel::Off) {
277 out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
278 }
279 }
280 out
281 }
282 }
283}
284
285pub fn validate_collection_upload_options(
292 opts: Option<&CollectionUploadOptions>,
293) -> Result<(), crate::swarm::Error> {
294 fn check(field: &str, value: &str) -> Result<(), crate::swarm::Error> {
295 for b in value.bytes() {
296 if b == b'\r' || b == b'\n' || b == 0 {
297 return Err(crate::swarm::Error::argument(format!(
298 "CollectionUploadOptions.{field} contains a forbidden byte (CR / LF / NUL)"
299 )));
300 }
301 }
302 Ok(())
303 }
304 if let Some(o) = opts {
305 if let Some(ref s) = o.index_document {
306 check("index_document", s)?;
307 }
308 if let Some(ref s) = o.error_document {
309 check("error_document", s)?;
310 }
311 }
312 Ok(())
313}
314
315pub fn prepare_collection_upload_headers(
322 batch_id: &BatchId,
323 opts: Option<&CollectionUploadOptions>,
324) -> HeaderPairs {
325 fn safe(s: &str) -> bool {
326 !s.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0)
327 }
328 match opts {
329 None => prepare_upload_headers(batch_id, None),
330 Some(o) => {
331 let mut out = prepare_upload_headers(batch_id, Some(&o.base));
332 if let Some(ref idx) = o.index_document {
333 if safe(idx) {
334 out.push(("Swarm-Index-Document", idx.clone()));
335 }
336 }
337 if let Some(ref err) = o.error_document {
338 if safe(err) {
339 out.push(("Swarm-Error-Document", err.clone()));
340 }
341 }
342 if let Some(level) = o.redundancy_level {
343 if !matches!(level, RedundancyLevel::Off) {
344 out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
345 }
346 }
347 out
348 }
349 }
350}
351
352pub fn prepare_download_headers(opts: Option<&DownloadOptions>) -> HeaderPairs {
356 let mut out = HeaderPairs::new();
357 let Some(o) = opts else { return out };
358
359 if let Some(s) = o.redundancy_strategy {
360 out.push(("Swarm-Redundancy-Strategy", s.as_u8().to_string()));
361 }
362 if let Some(v) = o.fallback {
363 out.push(("Swarm-Redundancy-Fallback-Mode", bool_str(v).to_string()));
364 }
365 if let Some(ms) = o.timeout_ms {
366 if ms > 0 {
367 out.push(("Swarm-Chunk-Retrieval-Timeout", ms.to_string()));
368 }
369 }
370 let mut act = false;
371 if let Some(ref pk) = o.act_publisher {
372 if let Ok(hex) = pk.compressed_hex() {
373 out.push(("Swarm-Act-Publisher", hex));
374 act = true;
375 }
376 }
377 if let Some(ref r) = o.act_history_address {
378 out.push(("Swarm-Act-History-Address", r.to_hex()));
379 act = true;
380 }
381 if let Some(ts) = o.act_timestamp {
382 if ts > 0 {
383 out.push(("Swarm-Act-Timestamp", ts.to_string()));
384 act = true;
385 }
386 }
387 if act {
388 out.push(("Swarm-Act", "true".to_string()));
389 }
390 out
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 fn batch() -> BatchId {
398 BatchId::new(&[0xab; 32]).unwrap()
399 }
400
401 fn header<'a>(h: &'a [(&'static str, String)], name: &str) -> Option<&'a str> {
402 h.iter().find(|(k, _)| *k == name).map(|(_, v)| v.as_str())
403 }
404
405 #[test]
406 fn upload_headers_omit_unset_fields() {
407 let h = prepare_upload_headers(&batch(), None);
408 assert_eq!(
409 header(&h, "Swarm-Postage-Batch-Id"),
410 Some("ab".repeat(32).as_str())
411 );
412 assert!(header(&h, "Swarm-Pin").is_none());
413 assert!(header(&h, "Swarm-Encrypt").is_none());
414 }
415
416 #[test]
417 fn upload_headers_distinguish_none_from_some_false() {
418 let opts = UploadOptions {
419 pin: Some(false),
420 ..Default::default()
421 };
422 let h = prepare_upload_headers(&batch(), Some(&opts));
423 assert_eq!(header(&h, "Swarm-Pin"), Some("false"));
424 }
425
426 #[test]
427 fn redundancy_level_off_is_omitted() {
428 let opts = RedundantUploadOptions {
429 redundancy_level: Some(RedundancyLevel::Off),
430 ..Default::default()
431 };
432 let h = prepare_redundant_upload_headers(&batch(), Some(&opts));
433 assert!(header(&h, "Swarm-Redundancy-Level").is_none());
434 }
435
436 #[test]
437 fn redundancy_level_medium_emits_header() {
438 let opts = RedundantUploadOptions {
439 redundancy_level: Some(RedundancyLevel::Medium),
440 ..Default::default()
441 };
442 let h = prepare_redundant_upload_headers(&batch(), Some(&opts));
443 assert_eq!(header(&h, "Swarm-Redundancy-Level"), Some("1"));
444 }
445
446 #[test]
447 fn collection_upload_uses_swarm_index_document_header() {
448 let opts = CollectionUploadOptions {
449 index_document: Some("index.html".into()),
450 ..Default::default()
451 };
452 let h = prepare_collection_upload_headers(&batch(), Some(&opts));
453 assert_eq!(header(&h, "Swarm-Index-Document"), Some("index.html"));
454 }
455
456 #[test]
457 fn download_act_implies_swarm_act_true() {
458 let opts = DownloadOptions {
459 act_history_address: Some(Reference::from_hex(&"00".repeat(32)).unwrap()),
460 ..Default::default()
461 };
462 let h = prepare_download_headers(Some(&opts));
463 assert_eq!(header(&h, "Swarm-Act"), Some("true"));
464 }
465
466 #[test]
467 fn download_no_options_no_headers() {
468 assert!(prepare_download_headers(None).is_empty());
469 assert!(prepare_download_headers(Some(&DownloadOptions::default())).is_empty());
470 }
471}