1use crate::error::Result;
6use crate::hash;
7use flate2::read::ZlibDecoder;
8use flate2::write::ZlibEncoder;
9use flate2::Compression;
10use std::io::{Read, Write};
11
12#[derive(Debug, Clone)]
14pub enum Card {
15 Login {
17 userid: String,
18 nonce: String,
19 signature: String,
20 },
21 Push {
23 servercode: String,
24 projectcode: String,
25 },
26 Pull {
28 servercode: String,
29 projectcode: String,
30 },
31 Clone {
33 protocol_version: Option<u32>,
34 sequence_number: Option<u64>,
35 },
36 File {
38 artifact_id: String,
39 delta_source: Option<String>,
40 content: Vec<u8>,
41 },
42 CFile {
44 artifact_id: String,
45 delta_source: Option<String>,
46 uncompressed_size: usize,
47 content: Vec<u8>,
48 },
49 Igot {
51 artifact_id: String,
52 is_private: bool,
53 },
54 Gimme { artifact_id: String },
56 Cookie { payload: String },
58 Error { message: String },
60 Message { text: String },
62 Pragma { name: String, values: Vec<String> },
64 Comment { text: String },
66 Private,
68 CloneSeqNo { sequence_number: u64 },
70 ReqConfig { name: String },
72 Config { name: String, content: Vec<u8> },
74}
75
76#[derive(Debug, Clone, Default)]
78pub struct Message {
79 pub cards: Vec<Card>,
80}
81
82impl Message {
83 pub fn new() -> Self {
85 Self { cards: Vec::new() }
86 }
87
88 pub fn add(&mut self, card: Card) {
90 self.cards.push(card);
91 }
92
93 pub fn encode(&self) -> Result<Vec<u8>> {
98 let text = self.to_text()?;
99 let uncompressed_size = text.len() as u32;
100 let compressed = compress(text.as_bytes())?;
101
102 let mut result = Vec::with_capacity(4 + compressed.len());
104 result.extend_from_slice(&uncompressed_size.to_be_bytes());
105 result.extend_from_slice(&compressed);
106
107 Ok(result)
108 }
109
110 pub fn to_text(&self) -> Result<String> {
112 let mut lines: Vec<String> = Vec::new();
113
114 for card in &self.cards {
115 lines.push(card.to_line()?);
116 }
117
118 Ok(lines.join(""))
119 }
120
121 pub fn decode(data: &[u8]) -> Result<Self> {
125 let bytes = decompress_bytes(data)?;
126 Self::from_bytes(&bytes)
127 }
128
129 pub fn from_bytes(data: &[u8]) -> Result<Self> {
133 let mut message = Message::new();
134 let mut pos = 0;
135
136 while pos < data.len() {
137 while pos < data.len()
139 && (data[pos] == b' ' || data[pos] == b'\n' || data[pos] == b'\r')
140 {
141 pos += 1;
142 }
143 if pos >= data.len() {
144 break;
145 }
146
147 let line_start = pos;
149 while pos < data.len() && data[pos] != b'\n' {
150 pos += 1;
151 }
152 let line_end = pos;
153 if pos < data.len() {
154 pos += 1; }
156
157 let line = String::from_utf8_lossy(&data[line_start..line_end]);
158 let line = line.trim();
159 if line.is_empty() {
160 continue;
161 }
162
163 let parts: Vec<&str> = line.splitn(2, ' ').collect();
164 let op = parts[0];
165 let args = parts.get(1).unwrap_or(&"");
166
167 match op {
168 "file" | "cfile" => {
169 let parts: Vec<&str> = args.split_whitespace().collect();
174 if parts.len() >= 2 {
175 let (artifact_id, delta_source, size) = if op == "cfile" {
176 if parts.len() >= 4 {
181 (
182 parts[0].to_string(),
183 Some(parts[1].to_string()),
184 parts[2].parse::<usize>().unwrap_or(0),
185 )
186 } else if parts.len() == 3 {
187 (
188 parts[0].to_string(),
189 None,
190 parts[1].parse::<usize>().unwrap_or(0),
191 )
192 } else {
193 continue;
194 }
195 } else {
196 if parts.len() >= 3 {
201 if parts[1].len() == 64
204 && parts[1].chars().all(|c| c.is_ascii_hexdigit())
205 {
206 (
208 parts[0].to_string(),
209 Some(parts[1].to_string()),
210 parts[2].parse::<usize>().unwrap_or(0),
211 )
212 } else {
213 (
215 parts[0].to_string(),
216 None,
217 parts[1].parse::<usize>().unwrap_or(0),
218 )
219 }
220 } else {
221 (
222 parts[0].to_string(),
223 None,
224 parts[1].parse::<usize>().unwrap_or(0),
225 )
226 }
227 };
228
229 let content_end = (pos + size).min(data.len());
231 let content = data[pos..content_end].to_vec();
232 pos = content_end;
233
234 if op == "cfile" {
235 message.add(Card::CFile {
236 artifact_id,
237 delta_source,
238 content,
239 uncompressed_size: parts
240 .last()
241 .and_then(|s| s.parse().ok())
242 .unwrap_or(0),
243 });
244 } else {
245 message.add(Card::File {
246 artifact_id,
247 delta_source,
248 content,
249 });
250 }
251 }
252 }
253 _ => {
254 if let Some(card) = Self::parse_text_card(op, args) {
256 message.add(card);
257 }
258 }
259 }
260 }
261
262 Ok(message)
263 }
264
265 fn parse_text_card(op: &str, args: &str) -> Option<Card> {
267 match op {
268 "login" => {
269 let parts: Vec<&str> = args.split_whitespace().collect();
270 if parts.len() >= 3 {
271 Some(Card::Login {
272 userid: decode_fossil_string(parts[0]),
273 nonce: parts[1].to_string(),
274 signature: parts[2].to_string(),
275 })
276 } else {
277 None
278 }
279 }
280 "push" => {
281 let parts: Vec<&str> = args.split_whitespace().collect();
282 if parts.len() >= 2 {
283 Some(Card::Push {
284 servercode: parts[0].to_string(),
285 projectcode: parts[1].to_string(),
286 })
287 } else {
288 None
289 }
290 }
291 "pull" => {
292 let parts: Vec<&str> = args.split_whitespace().collect();
293 if parts.len() >= 2 {
294 Some(Card::Pull {
295 servercode: parts[0].to_string(),
296 projectcode: parts[1].to_string(),
297 })
298 } else {
299 None
300 }
301 }
302 "clone" => {
303 let parts: Vec<&str> = args.split_whitespace().collect();
304 Some(Card::Clone {
305 protocol_version: parts.first().and_then(|s| s.parse().ok()),
306 sequence_number: parts.get(1).and_then(|s| s.parse().ok()),
307 })
308 }
309 "clone_seqno" => Some(Card::CloneSeqNo {
310 sequence_number: args.trim().parse().unwrap_or(0),
311 }),
312 "igot" => {
313 let parts: Vec<&str> = args.split_whitespace().collect();
314 if !parts.is_empty() {
315 Some(Card::Igot {
316 artifact_id: parts[0].to_string(),
317 is_private: parts.get(1).map(|&s| s == "1").unwrap_or(false),
318 })
319 } else {
320 None
321 }
322 }
323 "gimme" => Some(Card::Gimme {
324 artifact_id: args.trim().to_string(),
325 }),
326 "cookie" => Some(Card::Cookie {
327 payload: decode_fossil_string(args.trim()),
328 }),
329 "error" => Some(Card::Error {
330 message: decode_fossil_string(args.trim()),
331 }),
332 "pragma" => {
333 let parts: Vec<&str> = args.split_whitespace().collect();
334 if !parts.is_empty() {
335 Some(Card::Pragma {
336 name: parts[0].to_string(),
337 values: parts[1..].iter().map(|s| s.to_string()).collect(),
338 })
339 } else {
340 None
341 }
342 }
343 _ => {
344 None
346 }
347 }
348 }
349
350 pub fn from_text(text: &str) -> Result<Self> {
352 let mut message = Message::new();
353 let mut lines = text.lines().peekable();
354
355 while let Some(line) = lines.next() {
356 let line = line.trim();
357 if line.is_empty() {
358 continue;
359 }
360
361 let parts: Vec<&str> = line.splitn(2, ' ').collect();
362 let op = parts[0];
363 let args = parts.get(1).unwrap_or(&"");
364
365 match op {
366 "login" => {
367 let parts: Vec<&str> = args.split_whitespace().collect();
368 if parts.len() >= 3 {
369 message.add(Card::Login {
370 userid: decode_fossil_string(parts[0]),
371 nonce: parts[1].to_string(),
372 signature: parts[2].to_string(),
373 });
374 }
375 }
376 "push" => {
377 let parts: Vec<&str> = args.split_whitespace().collect();
378 if parts.len() >= 2 {
379 message.add(Card::Push {
380 servercode: parts[0].to_string(),
381 projectcode: parts[1].to_string(),
382 });
383 }
384 }
385 "pull" => {
386 let parts: Vec<&str> = args.split_whitespace().collect();
387 if parts.len() >= 2 {
388 message.add(Card::Pull {
389 servercode: parts[0].to_string(),
390 projectcode: parts[1].to_string(),
391 });
392 }
393 }
394 "clone" => {
395 let parts: Vec<&str> = args.split_whitespace().collect();
396 if parts.len() >= 2 {
397 message.add(Card::Clone {
398 protocol_version: parts[0].parse().ok(),
399 sequence_number: parts[1].parse().ok(),
400 });
401 } else {
402 message.add(Card::Clone {
403 protocol_version: None,
404 sequence_number: None,
405 });
406 }
407 }
408 "file" => {
409 let parts: Vec<&str> = args.split_whitespace().collect();
410 if parts.len() >= 2 {
411 let (artifact_id, delta_source, _size) = if parts.len() == 2 {
412 (
413 parts[0].to_string(),
414 None,
415 parts[1].parse::<usize>().unwrap_or(0),
416 )
417 } else {
418 (
419 parts[0].to_string(),
420 Some(parts[1].to_string()),
421 parts[2].parse::<usize>().unwrap_or(0),
422 )
423 };
424
425 message.add(Card::File {
428 artifact_id,
429 delta_source,
430 content: Vec::new(), });
432 }
433 }
434 "igot" => {
435 let parts: Vec<&str> = args.split_whitespace().collect();
436 if !parts.is_empty() {
437 let is_private = parts.get(1).map(|&s| s == "1").unwrap_or(false);
438 message.add(Card::Igot {
439 artifact_id: parts[0].to_string(),
440 is_private,
441 });
442 }
443 }
444 "gimme" => {
445 message.add(Card::Gimme {
446 artifact_id: args.trim().to_string(),
447 });
448 }
449 "cookie" => {
450 message.add(Card::Cookie {
451 payload: args.to_string(),
452 });
453 }
454 "error" => {
455 message.add(Card::Error {
456 message: decode_fossil_string(args),
457 });
458 }
459 "message" => {
460 message.add(Card::Message {
461 text: decode_fossil_string(args),
462 });
463 }
464 "pragma" => {
465 let parts: Vec<&str> = args.split_whitespace().collect();
466 if !parts.is_empty() {
467 message.add(Card::Pragma {
468 name: parts[0].to_string(),
469 values: parts[1..].iter().map(|s| s.to_string()).collect(),
470 });
471 }
472 }
473 "private" => {
474 message.add(Card::Private);
475 }
476 "clone_seqno" => {
477 if let Ok(seq) = args.trim().parse() {
478 message.add(Card::CloneSeqNo {
479 sequence_number: seq,
480 });
481 }
482 }
483 "reqconfig" => {
484 message.add(Card::ReqConfig {
485 name: args.trim().to_string(),
486 });
487 }
488 _ if op.starts_with('#') => {
489 message.add(Card::Comment {
490 text: line.to_string(),
491 });
492 }
493 _ => {
494 }
496 }
497 }
498
499 Ok(message)
500 }
501
502 pub fn create_login(userid: &str, password: &str, payload: &str) -> Card {
504 let nonce = hash::sha1_hex(payload.as_bytes());
506 let sig_input = format!("{}{}", nonce, password);
508 let signature = hash::sha1_hex(sig_input.as_bytes());
509
510 Card::Login {
511 userid: userid.to_string(),
512 nonce,
513 signature,
514 }
515 }
516}
517
518impl Card {
519 pub fn to_line(&self) -> Result<String> {
521 match self {
522 Card::Login {
523 userid,
524 nonce,
525 signature,
526 } => Ok(format!(
527 "login {} {} {}\n",
528 encode_fossil_string(userid),
529 nonce,
530 signature
531 )),
532 Card::Push {
533 servercode,
534 projectcode,
535 } => Ok(format!("push {} {}\n", servercode, projectcode)),
536 Card::Pull {
537 servercode,
538 projectcode,
539 } => Ok(format!("pull {} {}\n", servercode, projectcode)),
540 Card::Clone {
541 protocol_version,
542 sequence_number,
543 } => {
544 if let (Some(pv), Some(seq)) = (protocol_version, sequence_number) {
545 Ok(format!("clone {} {}\n", pv, seq))
546 } else {
547 Ok("clone\n".to_string())
548 }
549 }
550 Card::File {
551 artifact_id,
552 delta_source,
553 content,
554 } => {
555 if let Some(delta) = delta_source {
556 Ok(format!(
557 "file {} {} {}\n",
558 artifact_id,
559 delta,
560 content.len()
561 ))
562 } else {
563 Ok(format!("file {} {}\n", artifact_id, content.len()))
564 }
565 }
566 Card::CFile {
567 artifact_id,
568 delta_source,
569 uncompressed_size,
570 content,
571 } => {
572 if let Some(delta) = delta_source {
573 Ok(format!(
574 "cfile {} {} {} {}\n",
575 artifact_id,
576 delta,
577 uncompressed_size,
578 content.len()
579 ))
580 } else {
581 Ok(format!(
582 "cfile {} {} {}\n",
583 artifact_id,
584 uncompressed_size,
585 content.len()
586 ))
587 }
588 }
589 Card::Igot {
590 artifact_id,
591 is_private,
592 } => {
593 if *is_private {
594 Ok(format!("igot {} 1\n", artifact_id))
595 } else {
596 Ok(format!("igot {}\n", artifact_id))
597 }
598 }
599 Card::Gimme { artifact_id } => Ok(format!("gimme {}\n", artifact_id)),
600 Card::Cookie { payload } => Ok(format!("cookie {}\n", payload)),
601 Card::Error { message } => Ok(format!("error {}\n", encode_fossil_string(message))),
602 Card::Message { text } => Ok(format!("message {}\n", encode_fossil_string(text))),
603 Card::Pragma { name, values } => {
604 if values.is_empty() {
605 Ok(format!("pragma {}\n", name))
606 } else {
607 Ok(format!("pragma {} {}\n", name, values.join(" ")))
608 }
609 }
610 Card::Comment { text } => Ok(format!("{}\n", text)),
611 Card::Private => Ok("private\n".to_string()),
612 Card::CloneSeqNo { sequence_number } => {
613 Ok(format!("clone_seqno {}\n", sequence_number))
614 }
615 Card::ReqConfig { name } => Ok(format!("reqconfig {}\n", name)),
616 Card::Config { name, content } => Ok(format!("config {} {}\n", name, content.len())),
617 }
618 }
619
620 pub fn content_bytes(&self) -> Option<&[u8]> {
622 match self {
623 Card::File { content, .. } => Some(content),
624 Card::CFile { content, .. } => Some(content),
625 Card::Config { content, .. } => Some(content),
626 _ => None,
627 }
628 }
629}
630
631pub fn encode_fossil_string(s: &str) -> String {
633 s.replace('\\', "\\\\")
634 .replace(' ', "\\s")
635 .replace('\n', "\\n")
636}
637
638pub fn decode_fossil_string(s: &str) -> String {
640 let mut result = String::new();
641 let mut chars = s.chars().peekable();
642
643 while let Some(c) = chars.next() {
644 if c == '\\' {
645 match chars.next() {
646 Some('s') => result.push(' '),
647 Some('n') => result.push('\n'),
648 Some('\\') => result.push('\\'),
649 Some(other) => {
650 result.push('\\');
651 result.push(other);
652 }
653 None => result.push('\\'),
654 }
655 } else {
656 result.push(c);
657 }
658 }
659
660 result
661}
662
663pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
665 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
666 encoder.write_all(data)?;
667 Ok(encoder.finish()?)
668}
669
670fn get_compressed_data(data: &[u8]) -> &[u8] {
672 if data.len() >= 5 {
673 if data[4] == 0x78 {
676 return &data[4..];
678 }
679 }
680 if !data.is_empty() && data[0] == 0x78 {
681 return data;
683 }
684 data
686}
687
688pub fn decompress_bytes(data: &[u8]) -> Result<Vec<u8>> {
693 let compressed_data = get_compressed_data(data);
694 let mut decoder = ZlibDecoder::new(compressed_data);
695 let mut result = Vec::new();
696 decoder.read_to_end(&mut result)?;
697 Ok(result)
698}
699
700#[allow(dead_code)]
705pub fn decompress(data: &[u8]) -> Result<String> {
706 let compressed_data = get_compressed_data(data);
707 let mut decoder = ZlibDecoder::new(compressed_data);
708 let mut result = String::new();
709 decoder.read_to_string(&mut result)?;
710 Ok(result)
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn test_encode_decode_string() {
719 assert_eq!(encode_fossil_string("hello world"), "hello\\sworld");
720 assert_eq!(encode_fossil_string("line1\nline2"), "line1\\nline2");
721 assert_eq!(decode_fossil_string("hello\\sworld"), "hello world");
722 assert_eq!(decode_fossil_string("line1\\nline2"), "line1\nline2");
723 }
724
725 #[test]
726 fn test_message_parse() {
727 let text = "push abc123 def456\nigot hash1\nigot hash2 1\ngimme hash3\n";
728 let msg = Message::from_text(text).unwrap();
729 assert_eq!(msg.cards.len(), 4);
730 }
731
732 #[test]
733 fn test_compress_decompress() {
734 let original = "Hello, Heroforge sync protocol!";
735 let compressed = compress(original.as_bytes()).unwrap();
736 let decompressed = decompress(&compressed).unwrap();
737 assert_eq!(decompressed, original);
738 }
739}