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