1use super::error::{Result, VoidError};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12#[serde(default)]
13pub struct Config {
14 pub version: Option<u32>,
16 pub created: Option<String>,
18 #[serde(rename = "repoSecret")]
20 pub repo_secret: Option<String>,
21 #[serde(rename = "repoId", skip_serializing_if = "Option::is_none")]
23 pub repo_id: Option<String>,
24 #[serde(rename = "repoName", skip_serializing_if = "Option::is_none")]
26 pub repo_name: Option<String>,
27 pub ipfs: Option<IpfsConfig>,
29 pub tor: Option<TorConfig>,
31 pub user: UserConfig,
33 pub core: CoreConfig,
35 pub remote: HashMap<String, RemoteConfig>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41pub struct UserConfig {
42 pub name: Option<String>,
44 pub email: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(default)]
51pub struct CoreConfig {
52 pub compression_level: u8,
54 pub target_shard_size: usize,
56}
57
58impl Default for CoreConfig {
59 fn default() -> Self {
60 Self {
61 compression_level: 3,
62 target_shard_size: 100_000,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, Default)]
69#[serde(default)]
70pub struct IpfsConfig {
71 pub backend: Option<String>,
73 #[serde(rename = "kuboApi")]
75 pub kubo_api: Option<String>,
76 pub gateway: Option<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
85#[serde(default)]
86pub struct TorConfig {
87 pub mode: Option<String>,
89 #[serde(rename = "socksProxy")]
91 pub socks_proxy: Option<String>,
92 pub control: Option<String>,
94 #[serde(rename = "cookieAuth")]
96 pub cookie_auth: Option<bool>,
97 #[serde(rename = "hiddenService")]
99 pub hidden_service: Option<TorHiddenServiceConfig>,
100 pub kubo: Option<TorKuboConfig>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106#[serde(default)]
107pub struct TorHiddenServiceConfig {
108 pub enabled: Option<bool>,
110 #[serde(rename = "virtualPort")]
112 pub virtual_port: Option<u16>,
113 pub target: Option<String>,
115 pub hostname: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, Default)]
121#[serde(default)]
122pub struct TorKuboConfig {
123 pub enable: Option<bool>,
125 #[serde(rename = "setEnvProxy")]
127 pub set_env_proxy: Option<bool>,
128 #[serde(rename = "serviceName")]
130 pub service_name: Option<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, Default)]
141#[serde(default)]
142pub struct RemoteConfig {
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub url: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub host: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub user: Option<String>,
152 #[serde(rename = "keyPath", skip_serializing_if = "Option::is_none")]
154 pub key_path: Option<String>,
155 #[serde(rename = "peerMultiaddr", skip_serializing_if = "Option::is_none")]
157 pub peer_multiaddr: Option<String>,
158 #[serde(rename = "peerId", skip_serializing_if = "Option::is_none")]
160 pub peer_id: Option<String>,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
163 pub addrs: Vec<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub label: Option<String>,
167}
168
169const CONFIG_FILE: &str = "config.json";
171
172#[derive(Debug, Clone, Copy)]
173enum BasicConfigKey {
174 Version,
175 Created,
176 RepoSecret,
177 RepoId,
178 RepoName,
179 UserName,
180 UserEmail,
181 CoreCompressionLevel,
182 CoreTargetShardSize,
183 IpfsBackend,
184 IpfsKuboApi,
185 IpfsGateway,
186}
187
188impl BasicConfigKey {
189 const ALL: [Self; 12] = [
190 Self::Version,
191 Self::Created,
192 Self::RepoSecret,
193 Self::RepoId,
194 Self::RepoName,
195 Self::UserName,
196 Self::UserEmail,
197 Self::CoreCompressionLevel,
198 Self::CoreTargetShardSize,
199 Self::IpfsBackend,
200 Self::IpfsKuboApi,
201 Self::IpfsGateway,
202 ];
203
204 fn parse(parts: &[&str]) -> Option<Self> {
205 match parts {
206 ["version"] => Some(Self::Version),
207 ["created"] => Some(Self::Created),
208 ["repoSecret"] => Some(Self::RepoSecret),
209 ["repoId"] => Some(Self::RepoId),
210 ["repoName"] => Some(Self::RepoName),
211 ["user", "name"] => Some(Self::UserName),
212 ["user", "email"] => Some(Self::UserEmail),
213 ["core", "compression_level"] => Some(Self::CoreCompressionLevel),
214 ["core", "target_shard_size"] => Some(Self::CoreTargetShardSize),
215 ["ipfs", "backend"] => Some(Self::IpfsBackend),
216 ["ipfs", "kuboApi"] => Some(Self::IpfsKuboApi),
217 ["ipfs", "gateway"] => Some(Self::IpfsGateway),
218 _ => None,
219 }
220 }
221
222 fn dotted(self) -> &'static str {
223 match self {
224 Self::Version => "version",
225 Self::Created => "created",
226 Self::RepoSecret => "repoSecret",
227 Self::RepoId => "repoId",
228 Self::RepoName => "repoName",
229 Self::UserName => "user.name",
230 Self::UserEmail => "user.email",
231 Self::CoreCompressionLevel => "core.compression_level",
232 Self::CoreTargetShardSize => "core.target_shard_size",
233 Self::IpfsBackend => "ipfs.backend",
234 Self::IpfsKuboApi => "ipfs.kuboApi",
235 Self::IpfsGateway => "ipfs.gateway",
236 }
237 }
238
239 fn get(self, config: &Config) -> Option<String> {
240 match self {
241 Self::Version => config.version.map(|v| v.to_string()),
242 Self::Created => config.created.clone(),
243 Self::RepoSecret => config.repo_secret.clone(),
244 Self::RepoId => config.repo_id.clone(),
245 Self::RepoName => config.repo_name.clone(),
246 Self::UserName => config.user.name.clone(),
247 Self::UserEmail => config.user.email.clone(),
248 Self::CoreCompressionLevel => Some(config.core.compression_level.to_string()),
249 Self::CoreTargetShardSize => Some(config.core.target_shard_size.to_string()),
250 Self::IpfsBackend => config.ipfs.as_ref().and_then(|c| c.backend.clone()),
251 Self::IpfsKuboApi => config.ipfs.as_ref().and_then(|c| c.kubo_api.clone()),
252 Self::IpfsGateway => config.ipfs.as_ref().and_then(|c| c.gateway.clone()),
253 }
254 }
255
256 fn set(self, config: &mut Config, value: &str) -> Result<()> {
257 match self {
258 Self::Version | Self::Created | Self::RepoSecret | Self::RepoId | Self::RepoName => {
259 Err(VoidError::Serialization(format!(
260 "read-only config key: {}",
261 self.dotted()
262 )))
263 }
264 Self::UserName => {
265 config.user.name = Some(value.to_string());
266 Ok(())
267 }
268 Self::UserEmail => {
269 config.user.email = Some(value.to_string());
270 Ok(())
271 }
272 Self::CoreCompressionLevel => {
273 let level: u8 = value.parse().map_err(|_| {
274 VoidError::Serialization("invalid compression_level".to_string())
275 })?;
276 if level > 22 {
277 return Err(VoidError::Serialization(
278 "compression_level must be 0-22".to_string(),
279 ));
280 }
281 config.core.compression_level = level;
282 Ok(())
283 }
284 Self::CoreTargetShardSize => {
285 config.core.target_shard_size = value.parse().map_err(|_| {
286 VoidError::Serialization("invalid target_shard_size".to_string())
287 })?;
288 Ok(())
289 }
290 Self::IpfsBackend => {
291 config.ipfs.get_or_insert_with(IpfsConfig::default).backend =
292 Some(value.to_string());
293 Ok(())
294 }
295 Self::IpfsKuboApi => {
296 config.ipfs.get_or_insert_with(IpfsConfig::default).kubo_api =
297 Some(value.to_string());
298 Ok(())
299 }
300 Self::IpfsGateway => {
301 config.ipfs.get_or_insert_with(IpfsConfig::default).gateway =
302 Some(value.to_string());
303 Ok(())
304 }
305 }
306 }
307
308 fn unset(self, config: &mut Config) -> Result<()> {
309 match self {
310 Self::Version | Self::Created | Self::RepoSecret | Self::RepoId | Self::RepoName => {
311 Err(VoidError::Serialization(format!(
312 "read-only config key: {}",
313 self.dotted()
314 )))
315 }
316 Self::UserName => {
317 config.user.name = None;
318 Ok(())
319 }
320 Self::UserEmail => {
321 config.user.email = None;
322 Ok(())
323 }
324 Self::CoreCompressionLevel => {
325 config.core.compression_level = CoreConfig::default().compression_level;
326 Ok(())
327 }
328 Self::CoreTargetShardSize => {
329 config.core.target_shard_size = CoreConfig::default().target_shard_size;
330 Ok(())
331 }
332 Self::IpfsBackend => {
333 if let Some(ipfs) = config.ipfs.as_mut() {
334 ipfs.backend = None;
335 }
336 Ok(())
337 }
338 Self::IpfsKuboApi => {
339 if let Some(ipfs) = config.ipfs.as_mut() {
340 ipfs.kubo_api = None;
341 }
342 Ok(())
343 }
344 Self::IpfsGateway => {
345 if let Some(ipfs) = config.ipfs.as_mut() {
346 ipfs.gateway = None;
347 }
348 Ok(())
349 }
350 }
351 }
352}
353
354pub fn load(workspace: &Path) -> Result<Config> {
356 let config_path = workspace.join(CONFIG_FILE);
357
358 if !config_path.exists() {
359 return Ok(Config::default());
360 }
361
362 let content = std::fs::read_to_string(&config_path)?;
363 serde_json::from_str(&content).map_err(|e| VoidError::Serialization(e.to_string()))
364}
365
366pub fn save(workspace: &Path, config: &Config) -> Result<()> {
368 let config_path = workspace.join(CONFIG_FILE);
369 let content = serde_json::to_string_pretty(config)
370 .map_err(|e| VoidError::Serialization(e.to_string()))?;
371 std::fs::write(&config_path, content)?;
372 Ok(())
373}
374
375pub fn get(workspace: &Path, key: &str) -> Result<Option<String>> {
377 let config = load(workspace)?;
378 Ok(get_value(&config, key))
379}
380
381pub fn set(workspace: &Path, key: &str, value: &str) -> Result<()> {
383 let mut config = load(workspace)?;
384 set_value(&mut config, key, value)?;
385 save(workspace, &config)
386}
387
388pub fn unset(workspace: &Path, key: &str) -> Result<()> {
390 let mut config = load(workspace)?;
391 unset_value(&mut config, key)?;
392 save(workspace, &config)
393}
394
395pub fn list(workspace: &Path) -> Result<HashMap<String, String>> {
397 let config = load(workspace)?;
398 Ok(flatten_config(&config))
399}
400
401fn get_value(config: &Config, key: &str) -> Option<String> {
403 let parts: Vec<&str> = key.split('.').collect();
404 if let Some(value) = get_tor_value(config, parts.as_slice()) {
405 return Some(value);
406 }
407 if let Some(basic_key) = BasicConfigKey::parse(parts.as_slice()) {
408 return basic_key.get(config);
409 }
410
411 match parts.as_slice() {
412 ["remote", name, "url"] => config.remote.get(*name).and_then(|r| r.url.clone()),
413 ["remote", name, "host"] => config.remote.get(*name).and_then(|r| r.host.clone()),
414 ["remote", name, "user"] => config.remote.get(*name).and_then(|r| r.user.clone()),
415 ["remote", name, "keyPath"] => config.remote.get(*name).and_then(|r| r.key_path.clone()),
416 ["remote", name, "peerMultiaddr"] => config.remote.get(*name).and_then(|r| r.peer_multiaddr.clone()),
417 ["remote", name, "peerId"] => config.remote.get(*name).and_then(|r| r.peer_id.clone()),
418 ["remote", name, "label"] => config.remote.get(*name).and_then(|r| r.label.clone()),
419 _ => None,
420 }
421}
422
423fn set_value(config: &mut Config, key: &str, value: &str) -> Result<()> {
425 let parts: Vec<&str> = key.split('.').collect();
426 if let Some(result) = set_tor_value(config, parts.as_slice(), value) {
427 return result;
428 }
429 if let Some(basic_key) = BasicConfigKey::parse(parts.as_slice()) {
430 return basic_key.set(config, value);
431 }
432
433 match parts.as_slice() {
434 ["remote", name, "url"] => {
435 config.remote.entry((*name).to_string()).or_insert_with(RemoteConfig::default).url = Some(value.to_string());
436 }
437 ["remote", name, "host"] => {
438 config.remote.entry((*name).to_string()).or_insert_with(RemoteConfig::default).host = Some(value.to_string());
439 }
440 ["remote", name, "user"] => {
441 config.remote.entry((*name).to_string()).or_insert_with(RemoteConfig::default).user = Some(value.to_string());
442 }
443 ["remote", name, "keyPath"] => {
444 config.remote.entry((*name).to_string()).or_insert_with(RemoteConfig::default).key_path = Some(value.to_string());
445 }
446 ["remote", name, "peerMultiaddr"] => {
447 config.remote.entry((*name).to_string()).or_insert_with(RemoteConfig::default).peer_multiaddr = Some(value.to_string());
448 }
449 ["remote", name, "peerId"] => {
450 config.remote.entry((*name).to_string()).or_insert_with(RemoteConfig::default).peer_id = Some(value.to_string());
451 }
452 ["remote", name, "label"] => {
453 config.remote.entry((*name).to_string()).or_insert_with(RemoteConfig::default).label = Some(value.to_string());
454 }
455 _ => {
456 return Err(VoidError::Serialization(format!(
457 "unknown config key: {key}"
458 )));
459 }
460 }
461
462 Ok(())
463}
464
465fn unset_value(config: &mut Config, key: &str) -> Result<()> {
467 let parts: Vec<&str> = key.split('.').collect();
468 if let Some(result) = unset_tor_value(config, parts.as_slice()) {
469 return result;
470 }
471 if let Some(basic_key) = BasicConfigKey::parse(parts.as_slice()) {
472 return basic_key.unset(config);
473 }
474
475 match parts.as_slice() {
476 ["remote", name, "url"] => {
477 config.remote.remove(*name);
478 }
479 ["remote", name] => {
480 config.remote.remove(*name);
481 }
482 _ => {
483 return Err(VoidError::Serialization(format!(
484 "unknown config key: {key}"
485 )));
486 }
487 }
488
489 Ok(())
490}
491
492fn flatten_config(config: &Config) -> HashMap<String, String> {
494 let mut result = HashMap::new();
495
496 for basic_key in BasicConfigKey::ALL {
497 if let Some(value) = basic_key.get(config) {
498 result.insert(basic_key.dotted().to_string(), value);
499 }
500 }
501
502 if let Some(tor) = &config.tor {
503 flatten_tor_config(&mut result, tor);
504 }
505
506 for (name, remote) in &config.remote {
508 if let Some(url) = &remote.url {
509 result.insert(format!("remote.{name}.url"), url.clone());
510 }
511 if let Some(host) = &remote.host {
512 result.insert(format!("remote.{name}.host"), host.clone());
513 }
514 if let Some(user) = &remote.user {
515 result.insert(format!("remote.{name}.user"), user.clone());
516 }
517 if let Some(key_path) = &remote.key_path {
518 result.insert(format!("remote.{name}.keyPath"), key_path.clone());
519 }
520 if let Some(peer) = &remote.peer_multiaddr {
521 result.insert(format!("remote.{name}.peerMultiaddr"), peer.clone());
522 }
523 if let Some(peer_id) = &remote.peer_id {
524 result.insert(format!("remote.{name}.peerId"), peer_id.clone());
525 }
526 if let Some(label) = &remote.label {
527 result.insert(format!("remote.{name}.label"), label.clone());
528 }
529 }
530
531 result
532}
533
534fn get_tor_value(config: &Config, parts: &[&str]) -> Option<String> {
535 let tor = config.tor.as_ref()?;
536 match parts {
537 ["tor", "mode"] => tor.mode.clone(),
538 ["tor", "socksProxy"] => tor.socks_proxy.clone(),
539 ["tor", "control"] => tor.control.clone(),
540 ["tor", "cookieAuth"] => tor.cookie_auth.map(|v| v.to_string()),
541 ["tor", "hiddenService", "enabled"] => tor
542 .hidden_service
543 .as_ref()
544 .and_then(|hs| hs.enabled.map(|v| v.to_string())),
545 ["tor", "hiddenService", "virtualPort"] => tor
546 .hidden_service
547 .as_ref()
548 .and_then(|hs| hs.virtual_port.map(|v| v.to_string())),
549 ["tor", "hiddenService", "target"] => {
550 tor.hidden_service.as_ref().and_then(|hs| hs.target.clone())
551 }
552 ["tor", "hiddenService", "hostname"] => tor
553 .hidden_service
554 .as_ref()
555 .and_then(|hs| hs.hostname.clone()),
556 ["tor", "kubo", "enable"] => tor
557 .kubo
558 .as_ref()
559 .and_then(|k| k.enable.map(|v| v.to_string())),
560 ["tor", "kubo", "setEnvProxy"] => tor
561 .kubo
562 .as_ref()
563 .and_then(|k| k.set_env_proxy.map(|v| v.to_string())),
564 ["tor", "kubo", "serviceName"] => tor.kubo.as_ref().and_then(|k| k.service_name.clone()),
565 _ => None,
566 }
567}
568
569fn set_tor_value(config: &mut Config, parts: &[&str], value: &str) -> Option<Result<()>> {
570 match parts {
571 ["tor", "mode"] => Some(parse_tor_mode(value).map(|mode| {
572 tor_config_mut(config).mode = Some(mode);
573 })),
574 ["tor", "socksProxy"] => Some(Ok({
575 tor_config_mut(config).socks_proxy = Some(value.to_string());
576 })),
577 ["tor", "control"] => Some(Ok({
578 tor_config_mut(config).control = Some(value.to_string());
579 })),
580 ["tor", "cookieAuth"] => Some(parse_bool(value, "cookieAuth").map(|parsed| {
581 tor_config_mut(config).cookie_auth = Some(parsed);
582 })),
583 ["tor", "hiddenService", "enabled"] => {
584 Some(parse_bool(value, "hiddenService.enabled").map(|parsed| {
585 tor_hidden_service_mut(config).enabled = Some(parsed);
586 }))
587 }
588 ["tor", "hiddenService", "virtualPort"] => Some(
589 value
590 .parse::<u16>()
591 .map_err(|_| {
592 VoidError::Serialization("invalid hiddenService.virtualPort".to_string())
593 })
594 .map(|parsed| {
595 tor_hidden_service_mut(config).virtual_port = Some(parsed);
596 }),
597 ),
598 ["tor", "hiddenService", "target"] => Some(Ok({
599 tor_hidden_service_mut(config).target = Some(value.to_string());
600 })),
601 ["tor", "hiddenService", "hostname"] => Some(Ok({
602 tor_hidden_service_mut(config).hostname = Some(value.to_string());
603 })),
604 ["tor", "kubo", "enable"] => Some(parse_bool(value, "kubo.enable").map(|parsed| {
605 tor_kubo_mut(config).enable = Some(parsed);
606 })),
607 ["tor", "kubo", "setEnvProxy"] => {
608 Some(parse_bool(value, "kubo.setEnvProxy").map(|parsed| {
609 tor_kubo_mut(config).set_env_proxy = Some(parsed);
610 }))
611 }
612 ["tor", "kubo", "serviceName"] => Some(Ok({
613 tor_kubo_mut(config).service_name = Some(value.to_string());
614 })),
615 _ => None,
616 }
617}
618
619fn unset_tor_value(config: &mut Config, parts: &[&str]) -> Option<Result<()>> {
620 match parts {
621 ["tor", "mode"] => Some({
622 if let Some(tor) = config.tor.as_mut() {
623 tor.mode = None;
624 }
625 Ok(())
626 }),
627 ["tor", "socksProxy"] => Some({
628 if let Some(tor) = config.tor.as_mut() {
629 tor.socks_proxy = None;
630 }
631 Ok(())
632 }),
633 ["tor", "control"] => Some({
634 if let Some(tor) = config.tor.as_mut() {
635 tor.control = None;
636 }
637 Ok(())
638 }),
639 ["tor", "cookieAuth"] => Some({
640 if let Some(tor) = config.tor.as_mut() {
641 tor.cookie_auth = None;
642 }
643 Ok(())
644 }),
645 ["tor", "hiddenService", "enabled"] => Some({
646 if let Some(hidden) = config
647 .tor
648 .as_mut()
649 .and_then(|tor| tor.hidden_service.as_mut())
650 {
651 hidden.enabled = None;
652 }
653 Ok(())
654 }),
655 ["tor", "hiddenService", "virtualPort"] => Some({
656 if let Some(hidden) = config
657 .tor
658 .as_mut()
659 .and_then(|tor| tor.hidden_service.as_mut())
660 {
661 hidden.virtual_port = None;
662 }
663 Ok(())
664 }),
665 ["tor", "hiddenService", "target"] => Some({
666 if let Some(hidden) = config
667 .tor
668 .as_mut()
669 .and_then(|tor| tor.hidden_service.as_mut())
670 {
671 hidden.target = None;
672 }
673 Ok(())
674 }),
675 ["tor", "hiddenService", "hostname"] => Some({
676 if let Some(hidden) = config
677 .tor
678 .as_mut()
679 .and_then(|tor| tor.hidden_service.as_mut())
680 {
681 hidden.hostname = None;
682 }
683 Ok(())
684 }),
685 ["tor", "kubo", "enable"] => Some({
686 if let Some(kubo) = config.tor.as_mut().and_then(|tor| tor.kubo.as_mut()) {
687 kubo.enable = None;
688 }
689 Ok(())
690 }),
691 ["tor", "kubo", "setEnvProxy"] => Some({
692 if let Some(kubo) = config.tor.as_mut().and_then(|tor| tor.kubo.as_mut()) {
693 kubo.set_env_proxy = None;
694 }
695 Ok(())
696 }),
697 ["tor", "kubo", "serviceName"] => Some({
698 if let Some(kubo) = config.tor.as_mut().and_then(|tor| tor.kubo.as_mut()) {
699 kubo.service_name = None;
700 }
701 Ok(())
702 }),
703 _ => None,
704 }
705}
706
707fn flatten_tor_config(result: &mut HashMap<String, String>, tor: &TorConfig) {
708 if let Some(mode) = &tor.mode {
709 result.insert("tor.mode".to_string(), mode.clone());
710 }
711 if let Some(socks_proxy) = &tor.socks_proxy {
712 result.insert("tor.socksProxy".to_string(), socks_proxy.clone());
713 }
714 if let Some(control) = &tor.control {
715 result.insert("tor.control".to_string(), control.clone());
716 }
717 if let Some(cookie_auth) = tor.cookie_auth {
718 result.insert("tor.cookieAuth".to_string(), cookie_auth.to_string());
719 }
720 if let Some(hidden) = &tor.hidden_service {
721 if let Some(enabled) = hidden.enabled {
722 result.insert("tor.hiddenService.enabled".to_string(), enabled.to_string());
723 }
724 if let Some(port) = hidden.virtual_port {
725 result.insert(
726 "tor.hiddenService.virtualPort".to_string(),
727 port.to_string(),
728 );
729 }
730 if let Some(target) = &hidden.target {
731 result.insert("tor.hiddenService.target".to_string(), target.clone());
732 }
733 if let Some(hostname) = &hidden.hostname {
734 result.insert("tor.hiddenService.hostname".to_string(), hostname.clone());
735 }
736 }
737 if let Some(kubo) = &tor.kubo {
738 if let Some(enable) = kubo.enable {
739 result.insert("tor.kubo.enable".to_string(), enable.to_string());
740 }
741 if let Some(set_env_proxy) = kubo.set_env_proxy {
742 result.insert(
743 "tor.kubo.setEnvProxy".to_string(),
744 set_env_proxy.to_string(),
745 );
746 }
747 if let Some(service_name) = &kubo.service_name {
748 result.insert("tor.kubo.serviceName".to_string(), service_name.clone());
749 }
750 }
751}
752
753fn tor_config_mut(config: &mut Config) -> &mut TorConfig {
754 config.tor.get_or_insert_with(TorConfig::default)
755}
756
757fn tor_hidden_service_mut(config: &mut Config) -> &mut TorHiddenServiceConfig {
758 tor_config_mut(config)
759 .hidden_service
760 .get_or_insert_with(TorHiddenServiceConfig::default)
761}
762
763fn tor_kubo_mut(config: &mut Config) -> &mut TorKuboConfig {
764 tor_config_mut(config)
765 .kubo
766 .get_or_insert_with(TorKuboConfig::default)
767}
768
769fn parse_bool(value: &str, field: &str) -> Result<bool> {
770 value
771 .parse()
772 .map_err(|_| VoidError::Serialization(format!("invalid {field}")))
773}
774
775fn parse_tor_mode(value: &str) -> Result<String> {
776 match value {
777 "off" | "external" => Ok(value.to_string()),
778 _ => Err(VoidError::Serialization(
779 "tor.mode must be 'off' or 'external'".to_string(),
780 )),
781 }
782}
783
784
785pub fn load_repo_secret(void_dir: &Path, vault: &void_crypto::KeyVault) -> void_crypto::CryptoResult<void_crypto::RepoSecret> {
791 if let Ok(cfg) = load(void_dir) {
792 if let Some(secret_hex) = cfg.repo_secret {
793 if let Ok(bytes) = hex::decode(secret_hex.trim()) {
794 if let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) {
795 return Ok(void_crypto::RepoSecret::new(arr));
796 }
797 }
798 }
799 }
800 Ok(void_crypto::RepoSecret::new(*vault.repo_secret_fallback()?.as_bytes()))
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806 use tempfile::tempdir;
807
808 #[test]
809 fn test_default_config() {
810 let config = Config::default();
811 assert!(config.version.is_none());
812 assert!(config.created.is_none());
813 assert!(config.repo_secret.is_none());
814 assert!(config.ipfs.is_none());
815 assert!(config.tor.is_none());
816 assert!(config.user.name.is_none());
817 assert!(config.user.email.is_none());
818 assert_eq!(config.core.compression_level, 3);
819 assert_eq!(config.core.target_shard_size, 100_000);
820 assert!(config.remote.is_empty());
821 }
822
823 #[test]
824 fn test_save_and_load() {
825 let dir = tempdir().unwrap();
826 let workspace = dir.path();
827
828 let mut config = Config::default();
829 config.user.name = Some("Test User".to_string());
830 config.user.email = Some("test@example.com".to_string());
831
832 save(workspace, &config).unwrap();
833 let loaded = load(workspace).unwrap();
834
835 assert_eq!(loaded.user.name, Some("Test User".to_string()));
836 assert_eq!(loaded.user.email, Some("test@example.com".to_string()));
837 }
838
839 #[test]
840 fn test_get_set() {
841 let dir = tempdir().unwrap();
842 let workspace = dir.path();
843
844 set(workspace, "user.name", "Alice").unwrap();
846 assert_eq!(
847 get(workspace, "user.name").unwrap(),
848 Some("Alice".to_string())
849 );
850
851 set(workspace, "core.compression_level", "5").unwrap();
853 assert_eq!(
854 get(workspace, "core.compression_level").unwrap(),
855 Some("5".to_string())
856 );
857
858 set(workspace, "remote.origin.url", "https://example.com").unwrap();
860 assert_eq!(
861 get(workspace, "remote.origin.url").unwrap(),
862 Some("https://example.com".to_string())
863 );
864
865 set(workspace, "ipfs.backend", "kubo").unwrap();
867 assert_eq!(
868 get(workspace, "ipfs.backend").unwrap(),
869 Some("kubo".to_string())
870 );
871
872 set(workspace, "tor.mode", "external").unwrap();
874 assert_eq!(
875 get(workspace, "tor.mode").unwrap(),
876 Some("external".to_string())
877 );
878
879 set(workspace, "tor.cookieAuth", "true").unwrap();
881 assert_eq!(
882 get(workspace, "tor.cookieAuth").unwrap(),
883 Some("true".to_string())
884 );
885
886 set(workspace, "tor.hiddenService.virtualPort", "4001").unwrap();
888 assert_eq!(
889 get(workspace, "tor.hiddenService.virtualPort").unwrap(),
890 Some("4001".to_string())
891 );
892 }
893
894 #[test]
895 fn test_unset() {
896 let dir = tempdir().unwrap();
897 let workspace = dir.path();
898
899 set(workspace, "user.name", "Alice").unwrap();
900 assert!(get(workspace, "user.name").unwrap().is_some());
901
902 unset(workspace, "user.name").unwrap();
903 assert!(get(workspace, "user.name").unwrap().is_none());
904
905 set(workspace, "tor.mode", "external").unwrap();
906 assert_eq!(
907 get(workspace, "tor.mode").unwrap(),
908 Some("external".to_string())
909 );
910 unset(workspace, "tor.mode").unwrap();
911 assert!(get(workspace, "tor.mode").unwrap().is_none());
912 }
913
914 #[test]
915 fn test_list() {
916 let dir = tempdir().unwrap();
917 let workspace = dir.path();
918
919 set(workspace, "user.name", "Alice").unwrap();
920 set(workspace, "user.email", "alice@example.com").unwrap();
921 set(workspace, "remote.origin.url", "https://example.com").unwrap();
922 set(workspace, "ipfs.gateway", "https://dweb.link").unwrap();
923 set(workspace, "tor.socksProxy", "socks5h://127.0.0.1:9050").unwrap();
924
925 let all = list(workspace).unwrap();
926
927 assert_eq!(all.get("user.name"), Some(&"Alice".to_string()));
928 assert_eq!(
929 all.get("user.email"),
930 Some(&"alice@example.com".to_string())
931 );
932 assert_eq!(
933 all.get("remote.origin.url"),
934 Some(&"https://example.com".to_string())
935 );
936 assert_eq!(
937 all.get("ipfs.gateway"),
938 Some(&"https://dweb.link".to_string())
939 );
940 assert_eq!(
941 all.get("tor.socksProxy"),
942 Some(&"socks5h://127.0.0.1:9050".to_string())
943 );
944 assert!(all.contains_key("core.compression_level"));
946 assert!(all.contains_key("core.target_shard_size"));
947 }
948
949 #[test]
950 fn test_invalid_key() {
951 let dir = tempdir().unwrap();
952 let workspace = dir.path();
953
954 let result = set(workspace, "invalid.key", "value");
955 assert!(result.is_err());
956 }
957
958 #[test]
959 fn test_invalid_value() {
960 let dir = tempdir().unwrap();
961 let workspace = dir.path();
962
963 let result = set(workspace, "core.compression_level", "not_a_number");
964 assert!(result.is_err());
965 }
966
967 #[test]
968 fn test_invalid_tor_mode() {
969 let dir = tempdir().unwrap();
970 let workspace = dir.path();
971
972 let result = set(workspace, "tor.mode", "invalid");
973 assert!(result.is_err());
974 }
975}