1#![doc = include_str!("../README.md")]
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use ciborium;
7use brotli::{BrotliCompress, BrotliDecompress};
8use brotli::enc::BrotliEncoderParams;
9use log::{debug, error};
10
11mod id;
12pub use id::Id;
13
14mod resource;
15pub use resource::{Resource, ResourceValue};
16
17mod wallet;
18pub use wallet::{Wallet, WalletType};
19
20mod dns;
21pub use dns::Dns;
22
23#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
24pub struct SubDomain {
25 pub owner: String,
26 pub general: String,
27 pub twitter: String,
28 pub url: String,
29 pub nostr: String,
30 pub lightning: String,
31 pub btc: String,
32}
33
34#[derive(Debug, Serialize, Deserialize, Clone)]
41pub struct Wallets {
42 pub wallets: HashMap<WalletType, String> }
44
45#[derive(Debug, Serialize, Deserialize, Clone)]
46#[serde(tag = "v")]
47pub enum ZoneFile {
48 #[serde(rename = "0")]
49 V0 {
50 owner: String,
51 general: String,
52 twitter: String,
53 url: String,
54 nostr: String,
55 lightning: String,
56 btc: String,
57 subdomains: HashMap<String, SubDomain>,
58 },
59 #[serde(rename = "1")]
60 V1 {
61 #[serde(skip_serializing_if = "Option::is_none")]
62 default: Option<String>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
71 id: Option<Id>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
74 resources: Option<HashMap<String, Resource>>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
77 wallets: Option<HashMap<WalletType, String>>,
78
79 #[serde(skip_serializing_if = "crate::dns::is_dns_none")]
80 dns: Option<Dns>,
81 }
82}
83
84#[derive(Debug, Serialize, Deserialize, Clone)]
85pub struct Service {
86 pub name: String,
87 pub proof: String,
88}
89
90impl ZoneFile {
91 pub fn from_str(zonefile_str: &str) -> Result<Self, serde_json::Error> {
93 debug!("Attempting to parse zonefile from string");
94
95 if let Ok(v1) = serde_json::from_str::<Self>(zonefile_str) {
97 debug!("Successfully parsed V1 zonefile");
98 return Ok(v1);
99 }
100
101 debug!("V1 parsing failed, attempting to parse as legacy V0");
103 let legacy: Result<LegacyZoneFile, _> = serde_json::from_str(zonefile_str);
104 match legacy {
105 Ok(v0) => {
106 debug!("Converting legacy V0 to proper V0 with version tag");
107 let v0_json = serde_json::json!({
109 "v": "0",
110 "owner": v0.owner,
111 "general": v0.general,
112 "twitter": v0.twitter,
113 "url": v0.url,
114 "nostr": v0.nostr,
115 "lightning": v0.lightning,
116 "btc": v0.btc,
117 "subdomains": v0.subdomains,
118 });
119 let result = serde_json::from_value(v0_json);
120 if result.is_ok() {
121 debug!("Successfully parsed and converted legacy V0 zonefile");
122 } else {
123 error!("Failed to convert legacy V0 to proper V0 format");
124 }
125 result
126 },
127 Err(e) => {
128 error!("Failed to parse zonefile: {}", e);
129 Err(e)
130 }
131 }
132 }
133
134 pub fn to_string(&self) -> Result<String, serde_json::Error> {
136 debug!("Converting zonefile to JSON string");
137 let result = serde_json::to_string(self);
138 if let Err(ref e) = result {
139 error!("Failed to convert zonefile to string: {}", e);
140 } else {
141 debug!("Successfully converted zonefile to string");
142 }
143 result
144 }
145
146 pub fn new_v1(default: &str) -> Self {
148 debug!("Creating new V1 zonefile with default: {}", default);
149 ZoneFile::V1 { default: Some(default.to_string()), id: None, resources: None, wallets: None, dns: None }
150 }
151
152 pub fn new_v0(
154 owner: String,
155 general: String,
156 twitter: String,
157 url: String,
158 nostr: String,
159 lightning: String,
160 btc: String,
161 subdomains: HashMap<String, SubDomain>,
162 ) -> Self {
163 debug!("Creating new V0 zonefile for owner: {}", owner);
164 ZoneFile::V0 {
165 owner,
166 general,
167 twitter,
168 url,
169 nostr,
170 lightning,
171 btc,
172 subdomains,
173 }
174 }
175
176 pub fn from_clarity_buffer_hex_string(hex_string: &str) -> Result<Self, serde_json::Error> {
178 debug!("Parsing zonefile from clarity buffer hex string");
179 let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex_string);
180 let zonefile = ZoneFile::from_bytes(&bytes)?;
181 debug!("Successfully parsed zonefile from clarity buffer");
182 Ok(zonefile)
183 }
184
185 pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
186 debug!("Attempting to parse zonefile from {} bytes", bytes.len());
187
188 let mut decompressed = Vec::new();
190 let decompressed_result = {
191 let mut reader = bytes;
192 let mut writer = &mut decompressed;
193 BrotliDecompress(&mut reader, &mut writer)
194 };
195
196 let processed_bytes = match decompressed_result {
197 Ok(_) => {
198 debug!("Successfully decompressed brotli data");
199 decompressed
200 },
201 Err(e) => {
202 debug!("Brotli decompression failed ({}), using raw bytes", e);
203 bytes.to_vec()
204 }
205 };
206
207 debug!("Attempting to parse as CBOR");
209 match ZoneFile::from_cbor(&processed_bytes) {
210 Ok(zonefile) => {
211 debug!("Successfully parsed CBOR data");
212 Ok(zonefile)
213 },
214 Err(e) => {
215 debug!("CBOR parsing failed ({}), attempting JSON parsing", e);
216 match std::str::from_utf8(&processed_bytes) {
218 Ok(str_data) => {
219 debug!("Attempting to parse as JSON string");
220 let result = ZoneFile::from_str(str_data);
221 if result.is_ok() {
222 debug!("Successfully parsed JSON data");
223 } else {
224 error!("Failed to parse JSON data");
225 }
226 result
227 },
228 Err(e) => {
229 error!("Invalid UTF-8 in data: {}", e);
230 Err(serde_json::Error::io(std::io::Error::new(
231 std::io::ErrorKind::InvalidData,
232 format!("Invalid UTF-8: {}", e)
233 )))
234 }
235 }
236 }
237 }
238 }
239
240 pub fn from_cbor(cbor_bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>> {
242 debug!("Parsing zonefile from {} bytes of CBOR data", cbor_bytes.len());
243 let result = ciborium::de::from_reader(cbor_bytes);
244 if result.is_err() {
245 error!("Failed to parse CBOR data");
246 }
247 result
248 }
249
250 pub fn to_cbor(&self) -> Result<Vec<u8>, ciborium::ser::Error<std::io::Error>> {
252 debug!("Converting zonefile to CBOR");
253 let mut bytes = Vec::new();
254 let result = ciborium::ser::into_writer(self, &mut bytes);
255 if let Err(ref e) = result {
256 error!("Failed to convert to CBOR: {}", e);
257 } else {
258 debug!("Successfully converted to {} bytes of CBOR", bytes.len());
259 }
260 result.map(|_| bytes)
261 }
262
263 pub fn to_compressed_cbor(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
265 debug!("Converting zonefile to compressed CBOR");
266 let cbor_bytes = self.to_cbor()?;
267 let mut compressed = Vec::new();
268 {
269 let mut reader = &cbor_bytes[..];
270 let mut writer = &mut compressed;
271 let params = BrotliEncoderParams {
272 quality: 11, lgwin: 22, ..Default::default()
275 };
276 debug!("Compressing {} bytes of CBOR data", cbor_bytes.len());
277 match BrotliCompress(&mut reader, &mut writer, ¶ms) {
278 Ok(_) => {
279 debug!("Successfully compressed CBOR data: {} -> {} bytes", cbor_bytes.len(), compressed.len());
280 },
281 Err(e) => {
282 error!("Failed to compress CBOR data: {}", e);
283 return Err(Box::new(e));
284 }
285 }
286 }
287 Ok(compressed)
288 }
289
290 fn clarity_buffer_to_uint8_array(hex_string: &str) -> Vec<u8> {
291 debug!("Converting clarity buffer hex string to bytes");
292 let hex = hex_string.strip_prefix("0x").unwrap_or(hex_string);
294
295 let bytes: Vec<u8> = (0..hex.len())
297 .step_by(2)
298 .filter_map(|i| {
299 if i + 2 <= hex.len() {
300 u8::from_str_radix(&hex[i..i + 2], 16).ok()
301 } else {
302 None
303 }
304 })
305 .collect();
306
307 debug!("Converted {} hex chars to {} bytes", hex.len(), bytes.len());
308 bytes
309 }
310}
311
312#[derive(Debug, Serialize, Deserialize)]
314struct LegacyZoneFile {
315 owner: String,
316 general: String,
317 twitter: String,
318 url: String,
319 nostr: String,
320 lightning: String,
321 btc: String,
322 subdomains: HashMap<String, SubDomain>,
323}
324
325#[cfg(feature = "wasm")]
327pub mod wasm {
328 use super::*;
329 use wasm_bindgen::prelude::*;
330
331 #[wasm_bindgen]
332 pub fn parse_zonefile_from_clarity_buffer_hex_string(hex_string: &str) -> Result<Vec<u8>, JsValue> {
333 ZoneFile::from_clarity_buffer_hex_string(hex_string)
334 .and_then(|zf| zf.to_cbor().map_err(|e| serde_json::Error::io(std::io::Error::new(
335 std::io::ErrorKind::Other,
336 e.to_string()
337 ))))
338 .map_err(|e| JsValue::from_str(&e.to_string()))
339 }
340
341 #[wasm_bindgen(js_name = "generate_zonefile")]
342 pub fn generate_zonefile(val: JsValue, compress: bool) -> Result<Vec<u8>, JsValue> {
343 let zonefile: ZoneFile = serde_wasm_bindgen::from_value(val)
344 .map_err(|e| JsValue::from_str(&format!("Failed to parse zonefile: {}", e)))?;
345
346 if compress {
347 zonefile.to_compressed_cbor()
348 .map_err(|e| JsValue::from_str(&format!("Failed to compress zonefile: {}", e)))
349 } else {
350 zonefile.to_cbor()
351 .map_err(|e| JsValue::from_str(&format!("Failed to generate CBOR: {}", e)))
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::wallet::WalletType;
360
361 const SAMPLE_V0_ZONEFILE: &str = r#"{"owner":"SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8","general":"Pato Gómez","twitter":"@setpato","url":"pato.locker","nostr":"","lightning":"","btc":"bc1q4x98c6gwjj34lqs97zdvfyv2j0fcjhhw7hj0pc","subdomains":{"test":{"owner":"SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8","general":"test","twitter":"setpato","url":"testurl","nostr":"","lightning":"","btc":"bc1q4x98c6gwjj34lqs97zdvfyv2j0fcjhhw7hj0pc"}}}"#;
362 const SAMPLE_V0_ZONEFILE_EMPTY_SUBDOMAINS: &str = r#"{"owner":"SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8","general":"Pato Gómez","twitter":"@setpato","url":"pato.locker","nostr":"","lightning":"","btc":"bc1q4x98c6gwjj34lqs97zdvfyv2j0fcjhhw7hj0pc","subdomains":{}}"#;
363 const SAMPLE_ZONEFILE_HEX_STRING_LARRY_ID: &str = "7b226f776e6572223a2253503252374358454244474248374d374b3052484347514750593659364b524a314d37444541414d57222c2267656e6572616c223a22222c2274776974746572223a226c6172727973616c69627261222c2275726c223a22222c226e6f737472223a22222c226c696768746e696e67223a22222c22627463223a22222c22737562646f6d61696e73223a7b7d7d";
364 const SAMPLE_ZONEFILE_HEX_STRING_DEFAULT_ID: &str = "0x070a02000000148b0780a2617661316764656661756c7462696403";
365
366 fn setup() {
367 let _ = env_logger::builder()
368 .is_test(true)
369 .try_init();
370 }
371
372 #[test]
373 fn test_parse_zonefile_valid_v0() {
374 setup();
375 let result = ZoneFile::from_str(SAMPLE_V0_ZONEFILE);
376 assert!(result.is_ok());
377
378 let zonefile = result.unwrap();
379 match zonefile {
380 ZoneFile::V0 { owner, general, twitter, .. } => {
381 assert_eq!(&owner, "SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8");
382 assert_eq!(&general, "Pato Gómez");
383 assert_eq!(&twitter, "@setpato");
384 },
385 _ => panic!("Expected V0 zonefile"),
386 }
387 }
388
389 #[test]
390 #[ignore] fn test_parse_zonefile_valid_v1() {
392 setup();
393 debug!("Testing V1 zonefile parsing with hex string: {}", SAMPLE_ZONEFILE_HEX_STRING_DEFAULT_ID);
395
396 let result = ZoneFile::from_clarity_buffer_hex_string(SAMPLE_ZONEFILE_HEX_STRING_DEFAULT_ID);
397
398 if let Err(ref e) = result {
400 error!("Error parsing zonefile: {}", e);
401 }
402
403 assert!(result.is_ok());
404
405 let zonefile = result.unwrap();
406 debug!("Parsed zonefile: {:?}", zonefile);
407
408 match zonefile {
409 ZoneFile::V1 { default, .. } => {
410 assert_eq!(default, Some("id".to_string()));
411 },
412 _ => panic!("Expected V1 zonefile"),
413 }
414 }
415
416 #[test]
417 fn test_parse_zonefile_invalid() {
418 setup();
419 let result = ZoneFile::from_str("{invalid json}");
420 assert!(result.is_err());
421 }
422
423 #[test]
424 fn test_zonefile_roundtrip() {
425 setup();
426 let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE).unwrap();
427 let json = zonefile.to_string().unwrap();
428 let parsed_again = ZoneFile::from_str(&json).unwrap();
429
430 match (&zonefile, &parsed_again) {
431 (ZoneFile::V0 { owner: orig_owner, general: orig_general, subdomains: orig_subdomains, .. },
432 ZoneFile::V0 { owner: parsed_owner, general: parsed_general, subdomains: parsed_subdomains, .. }) => {
433 assert_eq!(orig_owner, parsed_owner);
434 assert_eq!(orig_general, parsed_general);
435
436 let orig_subdomain = orig_subdomains.get("test").unwrap();
437 let parsed_subdomain = parsed_subdomains.get("test").unwrap();
438 assert_eq!(orig_subdomain.general, parsed_subdomain.general);
439 assert_eq!(orig_subdomain.twitter, parsed_subdomain.twitter);
440 },
441 _ => panic!("Expected V0 zonefile"),
442 }
443 }
444
445 #[test]
446 fn test_cbor_roundtrip() {
447 setup();
448 let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE).unwrap();
449
450 let cbor_bytes = zonefile.to_cbor().unwrap();
452 let parsed_from_cbor = ZoneFile::from_bytes(&cbor_bytes).unwrap();
453 match (&zonefile, &parsed_from_cbor) {
454 (ZoneFile::V0 { owner: orig_owner, .. }, ZoneFile::V0 { owner: parsed_owner, .. }) => {
455 assert_eq!(orig_owner, parsed_owner);
456 },
457 _ => panic!("Expected V0 zonefile"),
458 }
459
460 let compressed_bytes = zonefile.to_compressed_cbor().unwrap();
462 let parsed_from_compressed = ZoneFile::from_bytes(&compressed_bytes).unwrap();
463 match (&zonefile, &parsed_from_compressed) {
464 (ZoneFile::V0 { owner: orig_owner, .. }, ZoneFile::V0 { owner: parsed_owner, .. }) => {
465 assert_eq!(orig_owner, parsed_owner);
466 },
467 _ => panic!("Expected V0 zonefile"),
468 }
469
470 assert!(compressed_bytes.len() < cbor_bytes.len());
472 debug!("CBOR size: {}, Compressed size: {}", cbor_bytes.len(), compressed_bytes.len());
473 }
474
475 #[test]
476 fn test_cbor_invalid_input() {
477 setup();
478 assert!(ZoneFile::from_cbor(&[]).is_err());
479 assert!(ZoneFile::from_cbor(&[0xFF, 0xFF, 0xFF]).is_err());
480 }
481
482 #[test]
483 fn test_cbor_size() {
484 setup();
485 let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE).unwrap();
486 let json_size = SAMPLE_V0_ZONEFILE.len();
487 let cbor_size = zonefile.to_cbor().unwrap().len();
488 assert!(cbor_size < json_size);
489 debug!("JSON size: {}, CBOR size: {}", json_size, cbor_size);
490 }
491
492 #[test]
493 fn test_cbor_empty_fields() {
494 setup();
495 let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE_EMPTY_SUBDOMAINS).unwrap();
496
497 let cbor_bytes = zonefile.to_cbor().unwrap();
498 let parsed_zonefile = ZoneFile::from_cbor(&cbor_bytes).unwrap();
499
500 match parsed_zonefile {
501 ZoneFile::V0 { nostr, lightning, subdomains, .. } => {
502 assert_eq!(nostr, "");
503 assert_eq!(lightning, "");
504 assert!(subdomains.is_empty());
505 },
506 _ => panic!("Expected V0 zonefile"),
507 }
508 }
509
510 #[test]
511 fn test_clarity_buffer_conversion() {
512 setup();
513 let hex = "0x5361746f736869"; let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
516 assert_eq!(bytes, b"Satoshi");
517
518 let hex = "5361746f736869"; let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
521 assert_eq!(bytes, b"Satoshi");
522
523 let hex = "";
525 let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
526 assert_eq!(bytes, Vec::<u8>::new());
527
528 let hex = "5361746f73686"; let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
531 assert_eq!(bytes, vec![0x53, 0x61, 0x74, 0x6f, 0x73, 0x68]); let hex = "0xZZ";
535 let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
536 assert_eq!(bytes, Vec::<u8>::new()); }
538
539 #[test]
540 fn test_from_clarity_buffer_hex_string() {
541 setup();
542 let hex = format!("0x{}", hex::encode(SAMPLE_V0_ZONEFILE));
544 debug!("Testing with hex string: {}", hex);
545
546 let result = ZoneFile::from_clarity_buffer_hex_string(&hex);
547 assert!(result.is_ok());
548 let zonefile = result.unwrap();
549 match zonefile {
550 ZoneFile::V0 { owner, general, .. } => {
551 assert_eq!(&owner, "SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8");
552 assert_eq!(&general, "Pato Gómez");
553 },
554 _ => panic!("Expected V0 zonefile"),
555 }
556
557 let result = ZoneFile::from_clarity_buffer_hex_string("0xZZ");
559 assert!(result.is_err());
560
561 let result = ZoneFile::from_clarity_buffer_hex_string("");
563 assert!(result.is_err());
564 }
565
566 #[test]
567 fn test_parse_hex_string_sample() {
568 setup();
569 let result = ZoneFile::from_clarity_buffer_hex_string(SAMPLE_ZONEFILE_HEX_STRING_LARRY_ID);
570 assert!(result.is_ok());
571
572 let zonefile = result.unwrap();
573 match zonefile {
574 ZoneFile::V0 { owner, general, twitter, url, nostr, lightning, btc, subdomains } => {
575 assert_eq!(owner, "SP2R7CXEBDGBH7M7K0RHCGQGPY6Y6KRJ1M7DEAAMW");
576 assert_eq!(general, "");
577 assert_eq!(twitter, "larrysalibra");
578 assert_eq!(url, "");
579 assert_eq!(nostr, "");
580 assert_eq!(lightning, "");
581 assert_eq!(btc, "");
582 assert!(subdomains.is_empty());
583 },
584 _ => panic!("Expected V0 zonefile"),
585 }
586 }
587
588 #[test]
589 fn test_version_handling() {
590 setup();
591 let json = r#"{"owner":"SP123","general":"Test","twitter":"@test","url":"test.url","nostr":"","lightning":"","btc":"","subdomains":{}}"#;
593 let zonefile = ZoneFile::from_str(json).unwrap();
594
595 let serialized = zonefile.to_string().unwrap();
597 debug!("Serialized V0 from legacy: {}", serialized);
598 assert!(serialized.contains(r#""v":"0""#), "Serialized output should contain version tag");
599
600 let json_v0 = r#"{"v":"0","owner":"SP123","general":"Test","twitter":"@test","url":"test.url","nostr":"","lightning":"","btc":"","subdomains":{}}"#;
602 let zonefile = ZoneFile::from_str(json_v0).unwrap();
603 let serialized = zonefile.to_string().unwrap();
604 debug!("Serialized V0 from explicit: {}", serialized);
605 assert!(serialized.contains(r#""v":"0""#), "Serialized output should contain version tag");
606
607 let cbor_bytes = zonefile.to_cbor().unwrap();
609 let parsed_from_cbor = ZoneFile::from_cbor(&cbor_bytes).unwrap();
610 let serialized_from_cbor = parsed_from_cbor.to_string().unwrap();
611 debug!("Serialized from CBOR: {}", serialized_from_cbor);
612 assert!(serialized_from_cbor.contains(r#""v":"0""#), "CBOR roundtrip should preserve version tag");
613
614 let json_v1 = r#"{"v":"1","default":"test.btc"}"#;
616 let zonefile = ZoneFile::from_str(json_v1).unwrap();
617 match zonefile {
618 ZoneFile::V1 { default, id: _, resources: _, wallets: _, dns: _ } => assert_eq!(default, Some("test.btc".to_string())),
619 _ => panic!("Expected V1 zonefile"),
620 }
621
622 let v1 = ZoneFile::new_v1("test.btc");
624 let serialized = v1.to_string().unwrap();
625 debug!("Serialized V1: {}", serialized);
626 assert!(serialized.contains(r#""v":"1""#), "Serialized V1 output should contain version tag");
627
628 let v0 = ZoneFile::new_v0(
630 "SP123".to_string(),
631 "Test".to_string(),
632 "@test".to_string(),
633 "test.url".to_string(),
634 "".to_string(),
635 "".to_string(),
636 "".to_string(),
637 HashMap::new(),
638 );
639 let serialized = v0.to_string().unwrap();
640 debug!("Serialized V0 from new: {}", serialized);
641 assert!(serialized.contains(r#""v":"0""#), "Serialized V0 output should contain version tag");
642 }
643
644 #[test]
645 fn test_version_tag_simple() {
646 setup();
647 let v0 = ZoneFile::new_v0(
649 "SP123".to_string(),
650 "Test".to_string(),
651 "@test".to_string(),
652 "test.url".to_string(),
653 "".to_string(),
654 "".to_string(),
655 "".to_string(),
656 HashMap::new(),
657 );
658 let serialized = serde_json::to_string(&v0).unwrap();
659 debug!("Direct V0 serialization: {}", serialized);
660 assert!(serialized.contains(r#""v":"0""#), "Direct V0 serialization should contain version tag");
661
662 let v1 = ZoneFile::new_v1("test.btc");
664 let serialized = serde_json::to_string(&v1).unwrap();
665 debug!("Direct V1 serialization: {}", serialized);
666 assert!(serialized.contains(r#""v":"1""#), "Direct V1 serialization should contain version tag");
667 }
668
669 #[test]
670 fn test_zonefile_v1_with_assets() {
671 setup();
672 let mut resources = HashMap::new();
673
674 resources.insert("greeting.txt".to_string(), Resource {
676 val: Some(ResourceValue::Text("Hello, world!".to_string())),
677 mime: Some("text/plain".to_string()),
678 });
679
680 resources.insert("data.bin".to_string(), Resource {
682 val: Some(ResourceValue::Binary(vec![0x01, 0x02, 0x03])),
683 mime: Some("application/octet-stream".to_string()),
684 });
685
686 let zonefile = ZoneFile::V1 {
687 default: Some("example.btc".to_string()),
688 id: None,
689 resources: Some(resources),
690 wallets: None,
691 dns: None,
692 };
693
694 let cbor_bytes = zonefile.to_cbor().unwrap();
696 let parsed_zonefile = ZoneFile::from_cbor(&cbor_bytes).unwrap();
697
698 match parsed_zonefile {
699 ZoneFile::V1 { resources: Some(parsed_resources), .. } => {
700 assert_eq!(parsed_resources.len(), 2);
701 assert!(parsed_resources.contains_key("greeting.txt"));
702 assert!(parsed_resources.contains_key("data.bin"));
703 },
704 _ => panic!("Expected V1 zonefile with resources"),
705 }
706 }
707
708 #[test]
709 fn test_v1_zonefile_with_wallets() {
710 setup();
711 let mut wallets = HashMap::new();
713 wallets.insert(WalletType::bitcoin(), "bc1qxxx...".to_string());
714 wallets.insert(WalletType::stacks(), "SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B".to_string());
715 wallets.insert(WalletType::solana(), "DxPv2QMA5cWR5Xj7BHt8J9LZkxGGx8DDsicafbLnGm9R".to_string());
716 wallets.insert(WalletType::ethereum(), "0x123...".to_string());
717 wallets.insert(WalletType::lightning(), "larry@getalby.com".to_string());
718
719 let avatar_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; let banner_data = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46]; let mut resources = HashMap::new();
725 resources.insert("avatar".to_string(), Resource {
726 val: Some(ResourceValue::Binary(avatar_data.clone())),
727 mime: Some("image/png".to_string()),
728 });
729 resources.insert("banner".to_string(), Resource {
730 val: Some(ResourceValue::Binary(banner_data.clone())),
731 mime: Some("image/jpeg".to_string()),
732 });
733
734 let zonefile = ZoneFile::V1 {
736 default: Some("larry.btc".to_string()),
737 id: None,
738 resources: Some(resources),
739 wallets: Some(wallets),
740 dns: None,
741 };
742
743 let json = serde_json::to_string_pretty(&zonefile).unwrap();
745 debug!("Complete V1 zonefile JSON:\n{}", json);
746
747 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
749
750 assert_eq!(parsed["v"], "1");
752
753 assert_eq!(parsed["default"], "larry.btc");
755
756 assert_eq!(parsed["wallets"]["0"], "bc1qxxx..."); assert_eq!(parsed["wallets"]["5757"], "SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B"); assert_eq!(parsed["wallets"]["501"], "DxPv2QMA5cWR5Xj7BHt8J9LZkxGGx8DDsicafbLnGm9R"); assert_eq!(parsed["wallets"]["60"], "0x123..."); assert_eq!(parsed["wallets"]["lightning"], "larry@getalby.com"); assert_eq!(parsed["resources"]["avatar"]["mime"], "image/png");
765 assert_eq!(parsed["resources"]["banner"]["mime"], "image/jpeg");
766
767 let deserialized: ZoneFile = serde_json::from_str(&json).unwrap();
769 match deserialized {
770 ZoneFile::V1 { default, id, resources, wallets, dns } => {
771 assert_eq!(default, Some("larry.btc".to_string()));
772 assert_eq!(id, None);
773 assert_eq!(dns, None);
774
775 let wallets = wallets.unwrap();
777 assert_eq!(wallets.get(&WalletType::bitcoin()).unwrap(), "bc1qxxx...");
778 assert_eq!(wallets.get(&WalletType::stacks()).unwrap(), "SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B");
779 assert_eq!(wallets.get(&WalletType::solana()).unwrap(), "DxPv2QMA5cWR5Xj7BHt8J9LZkxGGx8DDsicafbLnGm9R");
780 assert_eq!(wallets.get(&WalletType::ethereum()).unwrap(), "0x123...");
781 assert_eq!(wallets.get(&WalletType::lightning()).unwrap(), "larry@getalby.com");
782
783 let resources = resources.unwrap();
785 match &resources.get("avatar").unwrap().val {
786 Some(ResourceValue::Binary(data)) => assert_eq!(data, &avatar_data),
787 _ => panic!("Expected Binary resource value for avatar"),
788 }
789 assert_eq!(resources.get("avatar").unwrap().mime, Some("image/png".to_string()));
790
791 match &resources.get("banner").unwrap().val {
792 Some(ResourceValue::Binary(data)) => assert_eq!(data, &banner_data),
793 _ => panic!("Expected Binary resource value for banner"),
794 }
795 assert_eq!(resources.get("banner").unwrap().mime, Some("image/jpeg".to_string()));
796 },
797 _ => panic!("Expected V1 zonefile"),
798 }
799 }
800
801 #[test]
802 fn test_zonefile_v1_with_dns() {
803 setup();
804 let zonefile = ZoneFile::V1 {
806 default: Some("example.btc".to_string()),
807 id: None,
808 resources: None,
809 wallets: None,
810 dns: Some(Dns {
811 zone: Some("example.com".to_string()),
812 }),
813 };
814
815 let json = serde_json::to_string_pretty(&zonefile).unwrap();
817 debug!("V1 zonefile with DNS JSON:\n{}", json);
818
819 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
821 assert_eq!(parsed["v"], "1");
822 assert_eq!(parsed["default"], "example.btc");
823 assert_eq!(parsed["dns"], "example.com");
824
825 let zonefile = ZoneFile::V1 {
827 default: Some("example.btc".to_string()),
828 id: None,
829 resources: None,
830 wallets: None,
831 dns: Some(Dns { zone: None }),
832 };
833
834 let json = serde_json::to_string_pretty(&zonefile).unwrap();
835 debug!("V1 zonefile with empty DNS JSON:\n{}", json);
836
837 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
839 assert!(!parsed.as_object().unwrap().contains_key("dns"));
840
841 let zonefile = ZoneFile::V1 {
843 default: Some("example.btc".to_string()),
844 id: None,
845 resources: None,
846 wallets: None,
847 dns: None,
848 };
849
850 let json = serde_json::to_string_pretty(&zonefile).unwrap();
851 debug!("V1 zonefile without DNS JSON:\n{}", json);
852
853 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
855 assert!(!parsed.as_object().unwrap().contains_key("dns"));
856
857 let zonefile = ZoneFile::V1 {
859 default: Some("example.btc".to_string()),
860 id: None,
861 resources: None,
862 wallets: None,
863 dns: Some(Dns {
864 zone: Some("example.com".to_string()),
865 }),
866 };
867
868 let cbor_bytes = zonefile.to_cbor().unwrap();
869 let parsed_zonefile = ZoneFile::from_cbor(&cbor_bytes).unwrap();
870
871 match parsed_zonefile {
872 ZoneFile::V1 { dns: Some(dns), .. } => {
873 assert_eq!(dns.zone, Some("example.com".to_string()));
874 },
875 _ => panic!("Expected V1 zonefile with DNS"),
876 }
877 }
878}