1use std::fmt;
7use std::io::{self, Read, Write};
8use std::net::{SocketAddr, TcpStream};
9use std::time::Duration;
10
11const SAM_VERSION: &str = "3.1";
13
14const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
16
17const READ_TIMEOUT: Duration = Duration::from_secs(30);
19
20const I2P_BASE64_ALPHABET: &[u8; 64] =
24 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
25
26fn i2p_base64_decode_table() -> [u8; 256] {
28 let mut table = [255u8; 256];
29 for (i, &ch) in I2P_BASE64_ALPHABET.iter().enumerate() {
30 table[ch as usize] = i as u8;
31 }
32 table[b'=' as usize] = 0;
34 table
35}
36
37pub fn i2p_base64_encode(data: &[u8]) -> String {
39 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
40 for chunk in data.chunks(3) {
41 let b0 = chunk[0] as u32;
42 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
43 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
44 let triple = (b0 << 16) | (b1 << 8) | b2;
45
46 out.push(I2P_BASE64_ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
47 out.push(I2P_BASE64_ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
48
49 if chunk.len() > 1 {
50 out.push(I2P_BASE64_ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
51 } else {
52 out.push('=');
53 }
54
55 if chunk.len() > 2 {
56 out.push(I2P_BASE64_ALPHABET[(triple & 0x3F) as usize] as char);
57 } else {
58 out.push('=');
59 }
60 }
61 out
62}
63
64pub fn i2p_base64_decode(s: &str) -> Result<Vec<u8>, SamError> {
66 let table = i2p_base64_decode_table();
67 let bytes = s.as_bytes();
68
69 if !bytes.len().is_multiple_of(4) {
70 return Err(SamError::InvalidResponse(format!(
71 "invalid I2P base64 length: {}",
72 bytes.len()
73 )));
74 }
75
76 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
77
78 for chunk in bytes.chunks(4) {
79 let mut vals = [0u8; 4];
80 let mut pad_count = 0;
81 for (i, &b) in chunk.iter().enumerate() {
82 if b == b'=' {
83 pad_count += 1;
84 vals[i] = 0;
85 } else {
86 let v = table[b as usize];
87 if v == 255 {
88 return Err(SamError::InvalidResponse(format!(
89 "invalid I2P base64 character: {:?}",
90 b as char
91 )));
92 }
93 vals[i] = v;
94 }
95 }
96
97 let triple = (vals[0] as u32) << 18
98 | (vals[1] as u32) << 12
99 | (vals[2] as u32) << 6
100 | (vals[3] as u32);
101
102 out.push((triple >> 16) as u8);
103 if pad_count < 2 {
104 out.push((triple >> 8) as u8);
105 }
106 if pad_count < 1 {
107 out.push(triple as u8);
108 }
109 }
110
111 Ok(out)
112}
113
114#[derive(Debug)]
118pub enum SamError {
119 Io(io::Error),
121 Protocol(String),
123 InvalidResponse(String),
125}
126
127impl fmt::Display for SamError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 SamError::Io(e) => write!(f, "SAM I/O error: {}", e),
131 SamError::Protocol(msg) => write!(f, "SAM protocol error: {}", msg),
132 SamError::InvalidResponse(msg) => write!(f, "SAM invalid response: {}", msg),
133 }
134 }
135}
136
137impl From<io::Error> for SamError {
138 fn from(e: io::Error) -> Self {
139 SamError::Io(e)
140 }
141}
142
143#[derive(Clone, Debug)]
147pub struct Destination {
148 pub data: Vec<u8>,
150}
151
152impl Destination {
153 pub fn to_i2p_base64(&self) -> String {
155 i2p_base64_encode(&self.data)
156 }
157
158 pub fn from_i2p_base64(s: &str) -> Result<Self, SamError> {
160 let data = i2p_base64_decode(s)?;
161 Ok(Destination { data })
162 }
163
164 pub fn base32_address(&self) -> String {
167 let hash = rns_crypto::sha256::sha256(&self.data);
168 let encoded = base32_encode(&hash);
169 format!("{}.b32.i2p", encoded)
170 }
171}
172
173#[derive(Clone, Debug)]
175pub struct KeyPair {
176 pub destination: Destination,
177 pub private_key: Vec<u8>,
179}
180
181const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
184
185fn base32_encode(data: &[u8]) -> String {
187 let mut out = String::with_capacity((data.len() * 8).div_ceil(5));
188 let mut buffer: u64 = 0;
189 let mut bits: u32 = 0;
190
191 for &byte in data {
192 buffer = (buffer << 8) | byte as u64;
193 bits += 8;
194 while bits >= 5 {
195 bits -= 5;
196 out.push(BASE32_ALPHABET[((buffer >> bits) & 0x1F) as usize] as char);
197 }
198 }
199 if bits > 0 {
200 out.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1F) as usize] as char);
201 }
202 out
203}
204
205fn parse_kv(token: &str) -> Option<(&str, &str)> {
209 let eq = token.find('=')?;
210 Some((&token[..eq], &token[eq + 1..]))
211}
212
213fn read_line(stream: &mut TcpStream) -> Result<String, SamError> {
220 let mut line = Vec::new();
221 let mut byte = [0u8; 1];
222 loop {
223 match stream.read_exact(&mut byte) {
224 Ok(()) => {
225 if byte[0] == b'\n' {
226 break;
227 }
228 line.push(byte[0]);
229 }
230 Err(e) => return Err(SamError::Io(e)),
231 }
232 }
233 String::from_utf8(line)
234 .map_err(|e| SamError::InvalidResponse(format!("non-UTF8 SAM response: {}", e)))
235}
236
237fn hello_connect(sam_addr: &SocketAddr, underlay_mark: Option<u32>) -> Result<TcpStream, SamError> {
239 #[cfg(target_os = "linux")]
240 let mut stream = if underlay_mark.is_some() {
241 super::super::connect_tcp_with_options(sam_addr, None, underlay_mark, CONNECT_TIMEOUT)?
242 } else {
243 TcpStream::connect_timeout(sam_addr, CONNECT_TIMEOUT)?
244 };
245 #[cfg(not(target_os = "linux"))]
246 let mut stream = TcpStream::connect_timeout(sam_addr, CONNECT_TIMEOUT)?;
247 stream.set_read_timeout(Some(READ_TIMEOUT))?;
248 stream.set_write_timeout(Some(READ_TIMEOUT))?;
249
250 writeln!(stream, "HELLO VERSION MIN={v} MAX={v}", v = SAM_VERSION)?;
252 stream.flush()?;
253
254 let line = read_line(&mut stream)?;
256 let resp = parse_sam_response(&line)?;
258 if resp.command != "HELLO" || resp.subcommand != "REPLY" {
259 return Err(SamError::InvalidResponse(format!(
260 "expected HELLO REPLY, got: {}",
261 line
262 )));
263 }
264 check_result(&resp)?;
265
266 Ok(stream)
267}
268
269struct SamResponse {
271 command: String,
272 subcommand: String,
273 params: Vec<(String, String)>,
274}
275
276impl SamResponse {
277 fn get(&self, key: &str) -> Option<&str> {
278 for (k, v) in &self.params {
279 if k == key {
280 return Some(v);
281 }
282 }
283 None
284 }
285}
286
287fn parse_sam_response(line: &str) -> Result<SamResponse, SamError> {
291 if line.trim().is_empty() {
292 return Err(SamError::InvalidResponse("empty response".into()));
293 }
294 let mut parts = line.splitn(3, ' ');
295 let command = parts
296 .next()
297 .ok_or_else(|| SamError::InvalidResponse("empty response".into()))?
298 .to_string();
299 let subcommand = parts.next().unwrap_or("").to_string();
300 let rest = parts.next().unwrap_or("");
301
302 let mut params = Vec::new();
303 for token in rest.split_whitespace() {
306 if let Some((k, v)) = parse_kv(token) {
307 params.push((k.to_string(), v.to_string()));
308 }
309 }
310
311 Ok(SamResponse {
312 command,
313 subcommand,
314 params,
315 })
316}
317
318fn check_result(resp: &SamResponse) -> Result<(), SamError> {
320 match resp.get("RESULT") {
321 Some("OK") => Ok(()),
322 Some(result) => {
323 let message = resp.get("MESSAGE").unwrap_or("(no message)");
324 Err(SamError::Protocol(format!(
325 "RESULT={} MESSAGE={}",
326 result, message
327 )))
328 }
329 None => Ok(()), }
331}
332
333pub fn dest_generate(sam_addr: &SocketAddr) -> Result<KeyPair, SamError> {
338 dest_generate_with_mark(sam_addr, None)
339}
340
341pub(crate) fn dest_generate_with_mark(
342 sam_addr: &SocketAddr,
343 underlay_mark: Option<u32>,
344) -> Result<KeyPair, SamError> {
345 let mut stream = hello_connect(sam_addr, underlay_mark)?;
346
347 writeln!(stream, "DEST GENERATE SIGNATURE_TYPE=7")?;
348 stream.flush()?;
349
350 let line = read_line(&mut stream)?;
351 let resp = parse_sam_response(&line)?;
352
353 if resp.command != "DEST" || resp.subcommand != "REPLY" {
354 return Err(SamError::InvalidResponse(format!(
355 "expected DEST REPLY, got: {}",
356 line
357 )));
358 }
359
360 let pub_b64 = resp
361 .get("PUB")
362 .ok_or_else(|| SamError::InvalidResponse("DEST REPLY missing PUB".into()))?;
363 let priv_b64 = resp
364 .get("PRIV")
365 .ok_or_else(|| SamError::InvalidResponse("DEST REPLY missing PRIV".into()))?;
366
367 let dest_data = i2p_base64_decode(pub_b64)?;
368 let priv_data = i2p_base64_decode(priv_b64)?;
369
370 Ok(KeyPair {
371 destination: Destination { data: dest_data },
372 private_key: priv_data,
373 })
374}
375
376pub fn session_create(
379 sam_addr: &SocketAddr,
380 session_id: &str,
381 private_key_b64: &str,
382) -> Result<TcpStream, SamError> {
383 session_create_with_mark(sam_addr, session_id, private_key_b64, None)
384}
385
386pub(crate) fn session_create_with_mark(
387 sam_addr: &SocketAddr,
388 session_id: &str,
389 private_key_b64: &str,
390 underlay_mark: Option<u32>,
391) -> Result<TcpStream, SamError> {
392 let mut stream = hello_connect(sam_addr, underlay_mark)?;
393
394 writeln!(
395 stream,
396 "SESSION CREATE STYLE=STREAM ID={} DESTINATION={} SIGNATURE_TYPE=7",
397 session_id, private_key_b64,
398 )?;
399 stream.flush()?;
400
401 let line = read_line(&mut stream)?;
402 let resp = parse_sam_response(&line)?;
403
404 if resp.command != "SESSION" || resp.subcommand != "STATUS" {
405 return Err(SamError::InvalidResponse(format!(
406 "expected SESSION STATUS, got: {}",
407 line
408 )));
409 }
410 check_result(&resp)?;
411
412 Ok(stream)
414}
415
416pub fn stream_connect(
419 sam_addr: &SocketAddr,
420 session_id: &str,
421 destination: &str,
422) -> Result<TcpStream, SamError> {
423 stream_connect_with_mark(sam_addr, session_id, destination, None)
424}
425
426pub(crate) fn stream_connect_with_mark(
427 sam_addr: &SocketAddr,
428 session_id: &str,
429 destination: &str,
430 underlay_mark: Option<u32>,
431) -> Result<TcpStream, SamError> {
432 let mut stream = hello_connect(sam_addr, underlay_mark)?;
433
434 writeln!(
435 stream,
436 "STREAM CONNECT ID={} DESTINATION={} SILENT=false",
437 session_id, destination,
438 )?;
439 stream.flush()?;
440
441 let line = read_line(&mut stream)?;
442 let resp = parse_sam_response(&line)?;
443
444 if resp.command != "STREAM" || resp.subcommand != "STATUS" {
445 return Err(SamError::InvalidResponse(format!(
446 "expected STREAM STATUS, got: {}",
447 line
448 )));
449 }
450 check_result(&resp)?;
451
452 stream.set_read_timeout(None)?;
455 stream.set_write_timeout(None)?;
456
457 Ok(stream)
458}
459
460pub fn stream_accept(
463 sam_addr: &SocketAddr,
464 session_id: &str,
465) -> Result<(TcpStream, Destination), SamError> {
466 stream_accept_with_mark(sam_addr, session_id, None)
467}
468
469pub(crate) fn stream_accept_with_mark(
470 sam_addr: &SocketAddr,
471 session_id: &str,
472 underlay_mark: Option<u32>,
473) -> Result<(TcpStream, Destination), SamError> {
474 let mut stream = hello_connect(sam_addr, underlay_mark)?;
475
476 writeln!(stream, "STREAM ACCEPT ID={} SILENT=false", session_id,)?;
477 stream.flush()?;
478
479 let line = read_line(&mut stream)?;
480 let resp = parse_sam_response(&line)?;
481
482 if resp.command != "STREAM" || resp.subcommand != "STATUS" {
483 return Err(SamError::InvalidResponse(format!(
484 "expected STREAM STATUS, got: {}",
485 line
486 )));
487 }
488 check_result(&resp)?;
489
490 let dest_line = read_line(&mut stream)?;
493 let remote_dest = Destination::from_i2p_base64(dest_line.trim())?;
494
495 stream.set_read_timeout(None)?;
497 stream.set_write_timeout(None)?;
498
499 Ok((stream, remote_dest))
500}
501
502pub fn naming_lookup(sam_addr: &SocketAddr, name: &str) -> Result<Destination, SamError> {
505 naming_lookup_with_mark(sam_addr, name, None)
506}
507
508pub(crate) fn naming_lookup_with_mark(
509 sam_addr: &SocketAddr,
510 name: &str,
511 underlay_mark: Option<u32>,
512) -> Result<Destination, SamError> {
513 let mut stream = hello_connect(sam_addr, underlay_mark)?;
514 naming_lookup_on(&mut stream, name)
515}
516
517pub fn naming_lookup_on(stream: &mut TcpStream, name: &str) -> Result<Destination, SamError> {
521 writeln!(stream, "NAMING LOOKUP NAME={}", name)?;
522 stream.flush()?;
523
524 let line = read_line(stream)?;
525 let resp = parse_sam_response(&line)?;
526
527 if resp.command != "NAMING" || resp.subcommand != "REPLY" {
528 return Err(SamError::InvalidResponse(format!(
529 "expected NAMING REPLY, got: {}",
530 line
531 )));
532 }
533 check_result(&resp)?;
534
535 let value = resp
536 .get("VALUE")
537 .ok_or_else(|| SamError::InvalidResponse("NAMING REPLY missing VALUE".into()))?;
538
539 Destination::from_i2p_base64(value)
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use std::net::TcpListener;
546 use std::thread;
547
548 fn read_command(stream: &mut TcpStream) {
549 let mut byte = [0u8; 1];
550 loop {
551 stream.read_exact(&mut byte).unwrap();
552 if byte[0] == b'\n' {
553 return;
554 }
555 }
556 }
557
558 fn mock_sam(
559 command_reply: &'static str,
560 data_after_reply: Option<&'static str>,
561 ) -> (SocketAddr, thread::JoinHandle<()>) {
562 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
563 let address = listener.local_addr().unwrap();
564 let handle = thread::spawn(move || {
565 let (mut stream, _) = listener.accept().unwrap();
566 read_command(&mut stream);
567 stream
568 .write_all(b"HELLO REPLY RESULT=OK VERSION=3.1\n")
569 .unwrap();
570 read_command(&mut stream);
571 stream.write_all(command_reply.as_bytes()).unwrap();
572 stream.write_all(b"\n").unwrap();
573 if let Some(data) = data_after_reply {
574 stream.write_all(data.as_bytes()).unwrap();
575 stream.write_all(b"\n").unwrap();
576 }
577 stream.flush().unwrap();
578 stream
579 .set_read_timeout(Some(Duration::from_secs(2)))
580 .unwrap();
581 let mut byte = [0u8; 1];
582 assert_eq!(stream.read(&mut byte).unwrap(), 0);
583 });
584 (address, handle)
585 }
586
587 #[test]
590 fn base64_encode_empty() {
591 assert_eq!(i2p_base64_encode(b""), "");
592 }
593
594 #[test]
595 fn base64_roundtrip() {
596 let data: Vec<u8> = (0..=255).collect();
597 let encoded = i2p_base64_encode(&data);
598 let decoded = i2p_base64_decode(&encoded).unwrap();
599 assert_eq!(decoded, data);
600 }
601
602 #[test]
603 fn base64_known_value() {
604 let encoded = i2p_base64_encode(b"Hello");
607 assert_eq!(encoded, "SGVsbG8=");
608 let decoded = i2p_base64_decode(&encoded).unwrap();
609 assert_eq!(decoded, b"Hello");
610 }
611
612 #[test]
613 fn base64_i2p_specific_chars() {
614 let data = [0xFB, 0xEF, 0xBE];
617 let encoded = i2p_base64_encode(&data);
618 assert!(encoded.contains('-') || encoded.contains('~'));
619 let decoded = i2p_base64_decode(&encoded).unwrap();
621 assert_eq!(decoded, data);
622 }
623
624 #[test]
625 fn base64_all_alphabet_chars_roundtrip() {
626 let data: Vec<u8> = (0..48).collect();
628 let encoded = i2p_base64_encode(&data);
629 let decoded = i2p_base64_decode(&encoded).unwrap();
630 assert_eq!(decoded, data);
631 }
632
633 #[test]
634 fn base64_padding_1() {
635 let encoded = i2p_base64_encode(&[0xFF]);
637 assert_eq!(encoded.len(), 4);
638 assert!(encoded.ends_with("=="));
639 let decoded = i2p_base64_decode(&encoded).unwrap();
640 assert_eq!(decoded, vec![0xFF]);
641 }
642
643 #[test]
644 fn base64_padding_2() {
645 let encoded = i2p_base64_encode(&[0xFF, 0xFE]);
647 assert_eq!(encoded.len(), 4);
648 assert!(encoded.ends_with('='));
649 let decoded = i2p_base64_decode(&encoded).unwrap();
650 assert_eq!(decoded, vec![0xFF, 0xFE]);
651 }
652
653 #[test]
654 fn base64_no_padding() {
655 let encoded = i2p_base64_encode(&[0xFF, 0xFE, 0xFD]);
657 assert_eq!(encoded.len(), 4);
658 assert!(!encoded.contains('='));
659 let decoded = i2p_base64_decode(&encoded).unwrap();
660 assert_eq!(decoded, vec![0xFF, 0xFE, 0xFD]);
661 }
662
663 #[test]
664 fn base64_decode_invalid_char() {
665 let result = i2p_base64_decode("!!!=");
666 assert!(result.is_err());
667 }
668
669 #[test]
670 fn base64_decode_invalid_length() {
671 let result = i2p_base64_decode("ABC");
672 assert!(result.is_err());
673 }
674
675 #[test]
678 fn base32_encode_empty() {
679 assert_eq!(base32_encode(&[]), "");
680 }
681
682 #[test]
683 fn base32_encode_known() {
684 let result = base32_encode(b"Hello");
686 assert_eq!(result, "jbswy3dp");
687 }
688
689 #[test]
690 fn base32_encode_sha256() {
691 let hash = rns_crypto::sha256::sha256(b"");
693 let encoded = base32_encode(&hash);
694 assert_eq!(encoded.len(), 52);
696 assert!(encoded
698 .chars()
699 .all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c)));
700 }
701
702 #[test]
705 fn destination_base32_address() {
706 let dest = Destination {
707 data: vec![0x42; 387], };
709 let addr = dest.base32_address();
710 assert!(addr.ends_with(".b32.i2p"));
711 assert_eq!(addr.len(), 60);
713 }
714
715 #[test]
716 fn destination_roundtrip_base64() {
717 let data: Vec<u8> = (0..=255).cycle().take(387).collect();
718 let dest = Destination { data: data.clone() };
719 let b64 = dest.to_i2p_base64();
720 let dest2 = Destination::from_i2p_base64(&b64).unwrap();
721 assert_eq!(dest2.data, data);
722 }
723
724 #[test]
727 fn parse_hello_reply() {
728 let line = "HELLO REPLY RESULT=OK VERSION=3.1";
729 let resp = parse_sam_response(line).unwrap();
730 assert_eq!(resp.command, "HELLO");
731 assert_eq!(resp.subcommand, "REPLY");
732 assert_eq!(resp.get("RESULT"), Some("OK"));
733 assert_eq!(resp.get("VERSION"), Some("3.1"));
734 }
735
736 #[test]
737 fn parse_rejects_empty_and_whitespace_only_replies() {
738 assert!(matches!(
739 parse_sam_response(""),
740 Err(SamError::InvalidResponse(_))
741 ));
742 assert!(matches!(
743 parse_sam_response(" "),
744 Err(SamError::InvalidResponse(_))
745 ));
746 }
747
748 #[test]
749 fn session_create_reports_duplicate_id_and_closes_socket() {
750 let (address, server) = mock_sam("SESSION STATUS RESULT=DUPLICATED_ID", None);
751 let error = session_create(&address, "duplicate", "AAAA").unwrap_err();
752 assert!(matches!(error, SamError::Protocol(message) if message.contains("DUPLICATED_ID")));
753 server.join().unwrap();
754 }
755
756 #[test]
757 fn stream_connect_reports_unreachable_peer_and_closes_socket() {
758 let (address, server) = mock_sam("STREAM STATUS RESULT=CANT_REACH_PEER", None);
759 let error = stream_connect(&address, "session", "AAAA").unwrap_err();
760 assert!(
761 matches!(error, SamError::Protocol(message) if message.contains("CANT_REACH_PEER"))
762 );
763 server.join().unwrap();
764 }
765
766 #[test]
767 fn stream_accept_rejects_invalid_remote_destination_and_closes_socket() {
768 let (address, server) = mock_sam("STREAM STATUS RESULT=OK", Some("not-base64"));
769 let error = stream_accept(&address, "session").unwrap_err();
770 assert!(matches!(error, SamError::InvalidResponse(_)));
771 server.join().unwrap();
772 }
773
774 #[test]
775 fn parse_session_status_ok() {
776 let line = "SESSION STATUS RESULT=OK DESTINATION=AAAA";
777 let resp = parse_sam_response(line).unwrap();
778 assert_eq!(resp.command, "SESSION");
779 assert_eq!(resp.subcommand, "STATUS");
780 assert_eq!(resp.get("RESULT"), Some("OK"));
781 assert_eq!(resp.get("DESTINATION"), Some("AAAA"));
782 }
783
784 #[test]
785 fn parse_session_status_error() {
786 let line = "SESSION STATUS RESULT=DUPLICATED_ID";
787 let resp = parse_sam_response(line).unwrap();
788 assert_eq!(resp.get("RESULT"), Some("DUPLICATED_ID"));
789 let err = check_result(&resp);
790 assert!(err.is_err());
791 }
792
793 #[test]
794 fn parse_stream_status_error() {
795 let line = "STREAM STATUS RESULT=CANT_REACH_PEER MESSAGE=unreachable";
796 let resp = parse_sam_response(line).unwrap();
797 assert_eq!(resp.get("RESULT"), Some("CANT_REACH_PEER"));
798 assert_eq!(resp.get("MESSAGE"), Some("unreachable"));
799 let err = check_result(&resp);
800 assert!(err.is_err());
801 if let Err(SamError::Protocol(msg)) = err {
802 assert!(msg.contains("CANT_REACH_PEER"));
803 }
804 }
805
806 #[test]
807 fn parse_naming_reply() {
808 let line = "NAMING REPLY RESULT=OK NAME=test.b32.i2p VALUE=AAAA";
809 let resp = parse_sam_response(line).unwrap();
810 assert_eq!(resp.command, "NAMING");
811 assert_eq!(resp.subcommand, "REPLY");
812 assert_eq!(resp.get("NAME"), Some("test.b32.i2p"));
813 assert_eq!(resp.get("VALUE"), Some("AAAA"));
814 }
815
816 #[test]
817 fn parse_naming_not_found() {
818 let line = "NAMING REPLY RESULT=KEY_NOT_FOUND";
819 let resp = parse_sam_response(line).unwrap();
820 let err = check_result(&resp);
821 assert!(err.is_err());
822 }
823
824 #[test]
825 fn parse_dest_reply() {
826 let line = "DEST REPLY PUB=AAAA PRIV=BBBB";
827 let resp = parse_sam_response(line).unwrap();
828 assert_eq!(resp.command, "DEST");
829 assert_eq!(resp.subcommand, "REPLY");
830 assert_eq!(resp.get("PUB"), Some("AAAA"));
831 assert_eq!(resp.get("PRIV"), Some("BBBB"));
832 }
833
834 #[test]
835 fn parse_stream_status_timeout() {
836 let line = "STREAM STATUS RESULT=TIMEOUT";
837 let resp = parse_sam_response(line).unwrap();
838 let err = check_result(&resp);
839 assert!(err.is_err());
840 if let Err(SamError::Protocol(msg)) = err {
841 assert!(msg.contains("TIMEOUT"));
842 }
843 }
844
845 #[test]
846 fn check_result_ok() {
847 let line = "TEST REPLY RESULT=OK";
848 let resp = parse_sam_response(line).unwrap();
849 assert!(check_result(&resp).is_ok());
850 }
851
852 #[test]
853 fn check_result_no_result_field() {
854 let line = "TEST REPLY FOO=BAR";
855 let resp = parse_sam_response(line).unwrap();
856 assert!(check_result(&resp).is_ok());
858 }
859
860 #[test]
861 fn sam_error_display() {
862 let io_err = SamError::Io(io::Error::other("test"));
863 assert!(format!("{}", io_err).contains("test"));
864
865 let proto_err = SamError::Protocol("CANT_REACH_PEER".into());
866 assert!(format!("{}", proto_err).contains("CANT_REACH_PEER"));
867
868 let inv_err = SamError::InvalidResponse("bad".into());
869 assert!(format!("{}", inv_err).contains("bad"));
870 }
871}