weechat_relay_rs/
commands.rs

1pub use crate::basic_types::{Compression, PasswordHashAlgo, Pointer};
2
3use std::fmt::Write;
4
5/// A particular command, ready for sending.
6pub struct Command<T: CommandType> {
7    pub id: Option<String>,
8    pub command: T,
9}
10
11/// Some abstracted command, ready for sending.
12pub struct DynCommand {
13    pub id: Option<String>,
14    pub command: Box<dyn CommandType>,
15}
16
17impl<T: CommandType> Command<T> {
18    pub fn new(id: Option<String>, command: T) -> Self {
19        Command { id, command }
20    }
21}
22
23impl<T: CommandType> std::fmt::Display for Command<T> {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        let mut fields = Vec::with_capacity(2 + self.command.arguments().len());
26        if let Some(id) = &self.id {
27            fields.push(format!("({})", id));
28        }
29        fields.push(self.command.command().to_string());
30        fields.extend(self.command.arguments());
31        writeln!(f, "{}", fields.join(" "))
32    }
33}
34
35impl std::fmt::Display for DynCommand {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        if let Some(ref id) = self.id {
38            writeln!(f, "({}) {}", id, self.command)
39        } else {
40            self.command.fmt(f)
41        }
42    }
43}
44
45macro_rules! escaped {
46    ($self:ident) => {{
47        let mut fields = Vec::with_capacity(2 + $self.command.arguments().len());
48        if let Some(id) = &$self.id {
49            fields.push(format!("({})", id));
50        }
51        fields.push($self.command.command().to_string());
52
53        fields.extend(
54            $self
55                .command
56                .arguments()
57                .iter()
58                .map(|s| s.replace('\\', "\\\\").replace('\n', "\\n")),
59        );
60
61        let mut ret = fields.join(" ");
62        ret.push('\n');
63        ret
64    }};
65}
66
67impl<T: CommandType> Command<T> {
68    pub fn escaped(&self) -> String {
69        escaped!(self)
70    }
71}
72
73impl DynCommand {
74    pub fn escaped(&self) -> String {
75        escaped!(self)
76    }
77}
78
79pub trait CommandType {
80    fn command(&self) -> &'static str;
81    fn arguments(&self) -> Vec<String>;
82}
83
84impl std::fmt::Display for dyn CommandType {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        let mut fields = Vec::with_capacity(1 + self.arguments().len());
87        fields.push(self.command().to_string());
88        fields.extend(self.arguments());
89        writeln!(f, "{}", fields.join(" "))
90    }
91}
92
93/// The [handshake
94/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_handshake),
95/// sent before anything else in a session.
96///
97/// The handshake should be performed using [`Connection::new`](crate::Connection::new).
98///
99/// Response: [Hashtable](crate::messages::WHashtable)
100#[derive(Debug)]
101pub struct HandshakeCommand {
102    /// List of password hash algorithms this client is willing to accept.
103    pub password_hash_algo: Vec<PasswordHashAlgo>,
104    /// List of compresion algorithms this client is willing to accept.
105    pub compression: Vec<Compression>,
106    /// Whether commands sent should be escaped, allowing them to span multiple lines.
107    pub escape_commands: bool,
108}
109
110// we don't want to implement CommandType, else a Connection could establish that contradicts its parameters
111impl HandshakeCommand {
112    fn command(&self) -> &'static str {
113        "handshake"
114    }
115
116    fn arguments(&self) -> Vec<String> {
117        let mut ret = vec![];
118        if !self.password_hash_algo.is_empty() {
119            ret.push(format!(
120                "password_hash_algo={}",
121                self.password_hash_algo
122                    .iter()
123                    .map(PasswordHashAlgo::to_str)
124                    .collect::<Vec<&str>>()
125                    .join(":")
126            ));
127        }
128        if !self.compression.is_empty() {
129            ret.push(format!(
130                "compression={}",
131                self.compression
132                    .iter()
133                    .map(Compression::to_str)
134                    .collect::<Vec<&str>>()
135                    .join(":")
136            ));
137        }
138        if self.escape_commands {
139            ret.push("escape_commands=on".to_string());
140        }
141        if ret.is_empty() {
142            vec![]
143        } else {
144            vec![ret.join(",")]
145        }
146    }
147}
148
149impl std::fmt::Display for HandshakeCommand {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        let mut fields = Vec::with_capacity(1 + self.arguments().len());
152        fields.push(self.command().to_string());
153        fields.extend(self.arguments());
154        writeln!(f, "{}", fields.join(" "))
155    }
156}
157
158/// The [init
159/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_init), used
160/// to authenticate a session.
161///
162/// Response: None
163pub struct InitCommand {
164    /// Plaintext password authenticator. Probably mutually exclusive with `password_hash`.
165    pub password: Option<String>,
166    /// Hashed password. Probably mutually exclusive with `password`.
167    pub password_hash: Option<PasswordHash>,
168    /// Time-based One-Time Password. Typically combined with one of `password` or `password_hash`.
169    pub totp: Option<String>,
170}
171
172impl CommandType for InitCommand {
173    fn command(&self) -> &'static str {
174        "init"
175    }
176
177    fn arguments(&self) -> Vec<String> {
178        fn escape(arg: &str) -> String {
179            arg.replace(',', "\\,")
180        }
181        let mut ret = vec![];
182        let pw_hash: String;
183        if let Some(password) = &self.password {
184            ret.push(format!("password={}", escape(password)))
185        }
186        if let Some(password_hash) = &self.password_hash {
187            pw_hash = password_hash.to_string();
188            ret.push(format!("password_hash={}", pw_hash));
189        }
190        if let Some(totp) = &self.totp {
191            ret.push(format!("totp={}", escape(totp)));
192        }
193        vec![ret.join(",")]
194    }
195}
196
197/// The [hdata
198/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_hdata), used
199/// to request structured data.
200///
201/// Response: [Hdata](crate::messages::GenericHdata)
202pub struct HdataCommand {
203    /// The name of the requested hdata.
204    pub name: String,
205    /// A pointer or list name, forming the root of the path to the requested variable.
206    pub pointer: Countable<PointerOrName>,
207    /// A list of variable names that, with the pointer root, form the path to the requested
208    /// variable (the last in the path).
209    pub vars: Vec<Countable<String>>,
210    /// A list of keys to return in the hdata. An empty list returns all keys.
211    pub keys: Vec<String>,
212}
213
214impl HdataCommand {
215    fn path(&self) -> String {
216        if self.vars.is_empty() {
217            format!("{}:{}", self.name, self.pointer)
218        } else {
219            format!(
220                "{}:{}/{}",
221                self.name,
222                self.pointer,
223                self.vars
224                    .iter()
225                    .map(Countable::to_string)
226                    .collect::<Vec<String>>()
227                    .join("/")
228            )
229        }
230    }
231}
232
233impl CommandType for HdataCommand {
234    fn command(&self) -> &'static str {
235        "hdata"
236    }
237
238    fn arguments(&self) -> Vec<String> {
239        let mut args = vec![self.path()];
240        if !self.keys.is_empty() {
241            args.push(
242                self.keys
243                    .iter()
244                    .map(|s| s.as_str())
245                    .collect::<Vec<&str>>()
246                    .join(","),
247            );
248        }
249        args
250    }
251}
252
253/// The [info
254/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_info), used
255/// to request a single name/value pair.
256///
257/// Response: [Info](crate::messages::WInfo)
258pub struct InfoCommand {
259    /// Name of the info being requested.
260    pub name: String,
261    /// Arguments to the info request.
262    pub arguments: Vec<String>,
263}
264
265impl CommandType for InfoCommand {
266    fn command(&self) -> &'static str {
267        "info"
268    }
269
270    fn arguments(&self) -> Vec<String> {
271        let mut ret = vec![self.name.clone()];
272        ret.extend(self.arguments.iter().cloned());
273        ret
274    }
275}
276
277/// The [infolist
278/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_infolist),
279/// used to request a list of name/value pairs.
280///
281/// Response: [Infolist](crate::messages::WInfolist)
282pub struct InfolistCommand {
283    name: String,
284    // As of version 3.7 of the WeeChat Relay Protocol, if there are
285    // any arguments, the first argument *must* be a Pointer.
286    // Arguments to infolists without pointers can be accessed using a
287    // NULL pointer.
288    arguments: Option<(Pointer, Vec<String>)>,
289}
290
291impl InfolistCommand {
292    /// InfolistCommand constructor.
293    ///
294    /// `name`: The name of the infolist being requested.
295    ///
296    /// `arguments`: Arguments to the infolist request.
297    pub fn new(name: String, pointer: Option<Pointer>, arguments: Vec<String>) -> Self {
298        let arguments = match (pointer, !arguments.is_empty()) {
299            (None, false) => None,
300            (Some(p), _) => Some((p, arguments)),
301            (None, true) => Some((Pointer::null(), arguments)),
302        };
303        Self { name, arguments }
304    }
305}
306
307impl CommandType for InfolistCommand {
308    fn command(&self) -> &'static str {
309        "infolist"
310    }
311
312    fn arguments(&self) -> Vec<String> {
313        let mut ret = vec![self.name.clone()];
314        if let Some(arguments) = &self.arguments {
315            ret.push(arguments.0.to_string());
316            ret.extend(arguments.1.iter().cloned());
317        }
318        ret
319    }
320}
321
322/// The [nicklist
323/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_nicklist),
324/// used to request a nicklist for one or all buffers.
325///
326/// Response: [Hdata](crate::messages::GenericHdata)
327pub struct NicklistCommand {
328    pub buffer: Option<PointerOrName>,
329}
330
331impl CommandType for NicklistCommand {
332    fn command(&self) -> &'static str {
333        "nicklist"
334    }
335
336    fn arguments(&self) -> Vec<String> {
337        if let Some(buffer) = &self.buffer {
338            vec![buffer.to_string()]
339        } else {
340            vec![]
341        }
342    }
343}
344
345/// The [input
346/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_input), used
347/// to send data to a buffer.
348///
349/// Response: None
350pub struct InputCommand {
351    /// Pointer to or full name of the buffer.
352    pub buffer: PointerOrName,
353    /// String to input to the buffer.
354    pub data: String,
355}
356
357impl CommandType for InputCommand {
358    fn command(&self) -> &'static str {
359        "input"
360    }
361
362    fn arguments(&self) -> Vec<String> {
363        vec![self.buffer.to_string(), self.data.clone()]
364    }
365}
366
367/// The [completion
368/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_completion),
369/// used to request possible string completions.
370///
371/// Response: [Hdata](crate::messages::GenericHdata)
372pub struct CompletionCommand {
373    /// Pointer or name of the buffer to get completion from.
374    pub buffer: PointerOrName,
375    // if ever extended to negatives aside -1, extend this to i32
376    // so behavior of all currently working values is preserved
377    // (Idk what you're completing that's over 32,768 chars, but I'm not judging)
378    /// Position in the string for completion if `Some`, else complete at the end if `None`.
379    pub position: Option<u16>,
380    /// String to complete. `None` is the same as the empty string.
381    pub data: Option<String>,
382}
383
384impl CommandType for CompletionCommand {
385    fn command(&self) -> &'static str {
386        "completion"
387    }
388
389    fn arguments(&self) -> Vec<String> {
390        let position = match self.position {
391            Some(position) => position.to_string(),
392            None => "-1".to_string(),
393        };
394        let mut ret = vec![self.buffer.to_string(), position];
395        if let Some(data) = &self.data {
396            ret.push(data.clone());
397        }
398        ret
399    }
400}
401
402// At least in the current spec, "sync" and "desync" have identical invocations
403// (though different interpretations).
404macro_rules! sync_args {
405    ( $self:ident ) => {
406        match $self {
407            Self::AllBuffers(options) => {
408                if let Some(options) = options.to_string() {
409                    vec![options]
410                } else {
411                    vec![]
412                }
413            }
414            Self::SomeBuffers(buffers, options) => {
415                let mut args = vec![buffers
416                    .iter()
417                    .map(PointerOrName::to_string)
418                    .collect::<Vec<String>>()
419                    .join(",")];
420                match options {
421                    SyncSomeBuffers::Buffer => args.push("buffer".to_string()),
422                    SyncSomeBuffers::Nicklist => args.push("nicklist".to_string()),
423                    SyncSomeBuffers::All => (),
424                }
425                args
426            }
427        }
428    };
429}
430
431/// The [sync
432/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_sync), used
433/// to pull updates for one or more buffers.
434///
435/// Response: 0 or more [Hdatas](crate::messages::GenericHdata), received until a [`DesyncCommand`]
436/// is sent.
437pub enum SyncCommand {
438    AllBuffers(SyncAllBuffers),
439    SomeBuffers(Vec<PointerOrName>, SyncSomeBuffers),
440}
441
442impl CommandType for SyncCommand {
443    fn command(&self) -> &'static str {
444        "sync"
445    }
446
447    fn arguments(&self) -> Vec<String> {
448        sync_args!(self)
449    }
450}
451
452/// The [desync
453/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_desync),
454/// used to stop updates from one or more buffers.
455///
456/// Response: None
457pub enum DesyncCommand {
458    AllBuffers(SyncAllBuffers),
459    SomeBuffers(Vec<PointerOrName>, SyncSomeBuffers),
460}
461
462impl CommandType for DesyncCommand {
463    fn command(&self) -> &'static str {
464        "desync"
465    }
466
467    fn arguments(&self) -> Vec<String> {
468        sync_args!(self)
469    }
470}
471
472/// The [test
473/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_test), used
474/// to request sample objects for testing code.
475///
476/// Response (see linked docs above for values): [Char](crate::messages::WChar),
477/// [Integer](crate::messages::WInteger), [Integer](crate::messages::WInteger),
478/// [Long](crate::messages::WLongInteger), [Long](crate::messages::WLongInteger),
479/// [String](crate::messages::WString), [String](crate::messages::WString),
480/// [String](crate::messages::WString), [Buffer](crate::messages::WBuffer),
481/// [Buffer](crate::messages::WBuffer), [Pointer], [Pointer], [Time](crate::messages::WTime),
482/// [Array](crate::messages::WArray) ([String](crate::messages::WString)),
483/// [Array](crate::messages::WArray) ([Integer](crate::messages::WInteger))
484#[derive(Default)]
485pub struct TestCommand {}
486
487impl CommandType for TestCommand {
488    fn command(&self) -> &'static str {
489        "test"
490    }
491    fn arguments(&self) -> Vec<String> {
492        vec![]
493    }
494}
495
496/// The [ping
497/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_ping), used
498/// to test liveness and response time.
499///
500/// Response: [String](crate::messages::WString), with [Pong](crate::messages::Event::Pong)
501/// identifier.
502pub struct PingCommand {
503    pub argument: String,
504}
505
506impl CommandType for PingCommand {
507    fn command(&self) -> &'static str {
508        "ping"
509    }
510    fn arguments(&self) -> Vec<String> {
511        vec![self.argument.clone()]
512    }
513}
514
515/// The [quit
516/// command](https://weechat.org/files/doc/devel/weechat_relay_protocol.en.html#command_quit), used
517/// to disconnect from the relay.
518///
519/// Response: None
520#[derive(Default)]
521pub struct QuitCommand {}
522
523impl CommandType for QuitCommand {
524    fn command(&self) -> &'static str {
525        "quit"
526    }
527    fn arguments(&self) -> Vec<String> {
528        vec![]
529    }
530}
531
532/// Options for syncing/desyncing all buffers.
533pub struct SyncAllBuffers {
534    /// Whether to receive signals about buffers: open/closed, moved, renamed, merged/unmerged,
535    /// hidden/unhidden
536    pub buffers: bool,
537    /// Whether to receive signals about WeeChat upgrades (upgrade, upgrade ended)
538    pub upgrade: bool,
539    /// Whether to receive signals about each buffer (new lines, type changed, title changed, local
540    /// variable added/removed, plus everything in [`Self::buffers`].
541    pub buffer: bool,
542    /// Whether to receive updated nicklists when changed.
543    pub nicklist: bool,
544}
545
546impl SyncAllBuffers {
547    fn to_string(&self) -> Option<String> {
548        // spec specifically recommends (though doesn't require) handling the all case as the
549        // default case
550        if (self.buffers && self.upgrade && self.buffer && self.nicklist)
551            || (!self.buffers && !self.upgrade && !self.buffer && !self.nicklist)
552        {
553            return None;
554        }
555        let mut ret = vec![];
556        if self.buffers {
557            ret.push("buffers");
558        }
559        if self.upgrade {
560            ret.push("upgrade");
561        }
562        if self.buffer {
563            ret.push("buffer");
564        }
565        if self.nicklist {
566            ret.push("nicklist");
567        }
568        Some(format!("* {}", ret.join(",")))
569    }
570}
571
572/// Options for syncing/desyncing some buffers.
573pub enum SyncSomeBuffers {
574    /// Only receive signals about the buffer: new lines, type changed, title changed, local
575    /// variable added/removed, opened/closed, moved, renamed, merged/unmerged, hidden/unhidden.
576    Buffer,
577    /// Only receive updated nicklist when changed.
578    Nicklist,
579    /// Receive all of the above.
580    All,
581}
582
583/// A hashed password, with parameters.
584pub enum PasswordHash {
585    Sha256 {
586        salt: Vec<u8>,
587        hash: [u8; 32],
588    },
589    Sha512 {
590        salt: Vec<u8>,
591        hash: [u8; 64],
592    },
593    Pbkdf2Sha256 {
594        salt: Vec<u8>,
595        iterations: u32,
596        hash: [u8; 32],
597    },
598    Pbkdf2Sha512 {
599        salt: Vec<u8>,
600        iterations: u32,
601        hash: [u8; 64],
602    },
603}
604
605fn hex_encode(bytes: &[u8]) -> String {
606    bytes.iter().fold(String::new(), |mut output, b| {
607        let _ = write!(output, "{b:02x}");
608        output
609    })
610}
611
612impl std::fmt::Display for PasswordHash {
613    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614        match self {
615            PasswordHash::Sha256 { salt, hash } => {
616                write!(f, "sha256:{}:{}", hex_encode(salt), hex_encode(hash))
617            }
618            PasswordHash::Sha512 { salt, hash } => {
619                write!(f, "sha512:{}:{}", hex_encode(salt), hex_encode(hash))
620            }
621            PasswordHash::Pbkdf2Sha256 {
622                salt,
623                iterations,
624                hash,
625            } => write!(
626                f,
627                "pbkdf2+sha256:{}:{}:{}",
628                hex_encode(salt),
629                iterations,
630                hex_encode(hash)
631            ),
632            PasswordHash::Pbkdf2Sha512 {
633                salt,
634                iterations,
635                hash,
636            } => write!(
637                f,
638                "pbkdf2+sha512:{}:{}:{}",
639                hex_encode(salt),
640                iterations,
641                hex_encode(hash)
642            ),
643        }
644    }
645}
646
647/// The count of elements in an [`HdataCommand`].
648///
649/// Positive counts mean the next elements, negative counts mean the previous elements, a glob means
650/// the next elements to the end of the list.
651pub enum Count {
652    Count(i32),
653    Glob,
654}
655
656impl std::fmt::Display for Count {
657    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
658        match self {
659            Count::Count(c) => c.fmt(f),
660            Count::Glob => "*".fmt(f),
661        }
662    }
663}
664
665/// A countable element in an [`HdataCommand`], with its count.
666pub struct Countable<T: std::fmt::Display> {
667    pub count: Option<Count>,
668    pub object: T,
669}
670
671impl<T: std::fmt::Display> Countable<T> {
672    pub fn new(count: Option<Count>, object: T) -> Self {
673        Self { count, object }
674    }
675}
676
677impl<T: std::fmt::Display> std::fmt::Display for Countable<T> {
678    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679        match &self.count {
680            Some(count) => write!(f, "{}({})", self.object, count),
681            None => self.object.fmt(f),
682        }
683    }
684}
685
686/// A [`Pointer`] or name in root of the path of an [`HdataCommand`].
687pub enum PointerOrName {
688    Pointer(Pointer),
689    Name(String),
690}
691
692impl std::fmt::Display for PointerOrName {
693    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
694        match self {
695            PointerOrName::Pointer(pointer) => pointer.fmt(f),
696            PointerOrName::Name(string) => string.fmt(f),
697        }
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    // n.b. these tests only test the string commands they create,
706    // not any kind of interaction with a server
707    #[test]
708    fn test_handshake() {
709        use crate::basic_types::Compression;
710
711        let default_handshake = HandshakeCommand {
712            password_hash_algo: vec![],
713            compression: vec![],
714            escape_commands: false,
715        };
716        let compression_handshake = HandshakeCommand {
717            password_hash_algo: vec![],
718            compression: vec![Compression::Zstd],
719            escape_commands: false,
720        };
721        let all_hash_algos = vec![
722            PasswordHashAlgo::Plain,
723            PasswordHashAlgo::Sha256,
724            PasswordHashAlgo::Sha512,
725            PasswordHashAlgo::Pbkdf2Sha256,
726            PasswordHashAlgo::Pbkdf2Sha512,
727        ];
728        let full_handshake = HandshakeCommand {
729            password_hash_algo: all_hash_algos,
730            compression: vec![Compression::Zstd, Compression::Zlib, Compression::Off],
731            escape_commands: false,
732        };
733
734        assert_eq!(default_handshake.to_string(), "handshake\n");
735        assert_eq!(
736            compression_handshake.to_string(),
737            "handshake compression=zstd\n"
738        );
739        // FIXME: we shouldn't be testing the order of the options here,
740        // but should make sure however we end up testing checks for proper formatting
741        assert_eq!(
742            full_handshake.to_string(),
743            "handshake password_hash_algo=plain:sha256:sha512:pbkdf2+sha256:pbkdf2+sha512,compression=zstd:zlib:off\n"
744        );
745    }
746
747    #[test]
748    fn test_init() {
749        let normal_password = InitCommand {
750            password: Some("mypass".to_string()),
751            password_hash: None,
752            totp: None,
753        };
754        let password_with_commas = InitCommand {
755            password: Some("mypass,with,commas".to_string()),
756            password_hash: None,
757            totp: None,
758        };
759        let password_with_totp = InitCommand {
760            password: Some("mypass".to_string()),
761            password_hash: None,
762            totp: Some("123456".to_string()),
763        };
764
765        let salt = vec![
766            0x85, 0xb1, 0xee, 0x00, 0x69, 0x5a, 0x5b, 0x25, 0x4e, 0x14, 0xf4, 0x88, 0x55, 0x38,
767            0xdf, 0x0d, 0xa4, 0xb7, 0x32, 0x07, 0xf5, 0xaa, 0xe4,
768        ];
769        let salt_string = hex_encode(&salt);
770
771        let sha256_password = InitCommand {
772            password: None,
773            password_hash: Some(PasswordHash::Sha256 {
774                salt: salt.clone(),
775                hash: [
776                    0x2c, 0x6e, 0xd1, 0x2e, 0xb0, 0x10, 0x9f, 0xca, 0x3a, 0xed, 0xc0, 0x3b, 0xf0,
777                    0x3d, 0x9b, 0x6e, 0x80, 0x4c, 0xd6, 0x0a, 0x23, 0xe1, 0x73, 0x1f, 0xd1, 0x77,
778                    0x94, 0xda, 0x42, 0x3e, 0x21, 0xdb,
779                ],
780            }),
781            totp: None,
782        };
783
784        let sha512_password = InitCommand {
785            password: None,
786            password_hash: Some(PasswordHash::Sha512 {
787                salt: salt.clone(),
788                hash: [
789                    0x0a, 0x1f, 0x01, 0x72, 0xa5, 0x42, 0x91, 0x6b, 0xd8, 0x6e, 0x0c, 0xbc, 0xee,
790                    0xbc, 0x1c, 0x38, 0xed, 0x79, 0x1f, 0x6b, 0xe2, 0x46, 0x12, 0x04, 0x52, 0x82,
791                    0x5f, 0x0d, 0x74, 0xef, 0x10, 0x78, 0xc7, 0x9e, 0x98, 0x12, 0xde, 0x8b, 0x0a,
792                    0xb3, 0xdf, 0xaf, 0x59, 0x8b, 0x6c, 0xa1, 0x45, 0x22, 0x37, 0x4e, 0xc6, 0xa8,
793                    0x65, 0x3a, 0x46, 0xdf, 0x3f, 0x96, 0xa6, 0xb5, 0x4a, 0xc1, 0xf0, 0xf8,
794                ],
795            }),
796            totp: None,
797        };
798
799        let pbkdf2_password = InitCommand {
800            password: None,
801            password_hash: Some(PasswordHash::Pbkdf2Sha256 {
802                salt,
803                iterations: 100000,
804                hash: [
805                    0xba, 0x7f, 0xac, 0xc3, 0xed, 0xb8, 0x9c, 0xd0, 0x6a, 0xe8, 0x10, 0xe2, 0x9c,
806                    0xed, 0x85, 0x98, 0x0f, 0xf3, 0x6d, 0xe2, 0xbb, 0x59, 0x6f, 0xcf, 0x51, 0x3a,
807                    0xaa, 0xb6, 0x26, 0x87, 0x64, 0x40,
808                ],
809            }),
810            totp: None,
811        };
812
813        let command = Command::new(None, normal_password);
814        assert_eq!(command.to_string(), "init password=mypass\n");
815
816        let command = Command::new(None, password_with_commas);
817        assert_eq!(
818            command.to_string(),
819            "init password=mypass\\,with\\,commas\n"
820        );
821
822        let command = Command::new(None, password_with_totp);
823        assert_eq!(command.to_string(), "init password=mypass,totp=123456\n");
824
825        let command = Command::new(None, sha256_password);
826        let hash = "2c6ed12eb0109fca3aedc03bf03d9b6e804cd60a23e1731fd17794da423e21db";
827        assert_eq!(
828            command.to_string(),
829            format!("init password_hash=sha256:{salt_string}:{hash}\n")
830        );
831
832        let command = Command::new(None, sha512_password);
833        let hash = "0a1f0172a542916bd86e0cbceebc1c38ed791f6be246120452825f0d74ef1078c79e9812de8b0ab3dfaf598b6ca14522374ec6a8653a46df3f96a6b54ac1f0f8";
834        assert_eq!(
835            command.to_string(),
836            format!("init password_hash=sha512:{salt_string}:{hash}\n")
837        );
838
839        let command = Command::new(None, pbkdf2_password);
840        let hash = "ba7facc3edb89cd06ae810e29ced85980ff36de2bb596fcf513aaab626876440";
841        assert_eq!(
842            command.to_string(),
843            format!("init password_hash=pbkdf2+sha256:{salt_string}:100000:{hash}\n")
844        );
845    }
846
847    #[test]
848    fn test_hdata() {
849        let hdata_buffers = HdataCommand {
850            name: "buffer".to_string(),
851            pointer: Countable::new(
852                Some(Count::Glob),
853                PointerOrName::Name("gui_buffers".to_string()),
854            ),
855            vars: vec![],
856            keys: vec!["number".to_string(), "full_name".to_string()],
857        };
858
859        let hdata_lines = HdataCommand {
860            name: "buffer".to_string(),
861            pointer: Countable::new(None, PointerOrName::Name("gui_buffers".to_string())),
862            vars: vec![
863                Countable::new(None, "own_lines".to_string()),
864                Countable::new(Some(Count::Glob), "first_line".to_string()),
865                Countable::new(None, "data".to_string()),
866            ],
867            keys: vec![],
868        };
869
870        let hdata_hotlist = HdataCommand {
871            name: "hotlist".to_string(),
872            pointer: Countable::new(
873                Some(Count::Glob),
874                PointerOrName::Name("gui_hotlist".to_string()),
875            ),
876            vars: vec![],
877            keys: vec![],
878        };
879
880        let command = Command::new(Some("hdata_buffers".to_string()), hdata_buffers);
881        assert_eq!(
882            command.to_string(),
883            "(hdata_buffers) hdata buffer:gui_buffers(*) number,full_name\n"
884        );
885
886        let command = Command::new(Some("hdata_lines".to_string()), hdata_lines);
887        assert_eq!(
888            command.to_string(),
889            "(hdata_lines) hdata buffer:gui_buffers/own_lines/first_line(*)/data\n"
890        );
891
892        let command = Command::new(Some("hdata_hotlist".to_string()), hdata_hotlist);
893        assert_eq!(
894            command.to_string(),
895            "(hdata_hotlist) hdata hotlist:gui_hotlist(*)\n"
896        );
897    }
898
899    #[test]
900    fn test_info() {
901        let info = InfoCommand {
902            name: "version".to_string(),
903            arguments: vec![],
904        };
905        let command = Command::new(Some("info_version".to_string()), info);
906        assert_eq!(command.to_string(), "(info_version) info version\n");
907
908        let info = InfoCommand {
909            name: "nick_color".to_string(),
910            arguments: vec!["foo".to_string()],
911        };
912        let command = Command::new(Some("foo_color".to_string()), info);
913        assert_eq!(command.to_string(), "(foo_color) info nick_color foo\n");
914    }
915
916    #[test]
917    fn test_infolist() {
918        let id = "infolist_buffer".to_string();
919        let name = "buffer".to_string();
920        let pointer = Pointer::new("1234abcd".as_bytes().to_vec()).expect("invalid pointer");
921        let arguments = vec!["core.weechat".to_string()];
922
923        let infolist_buffer = InfolistCommand::new(name.clone(), None, vec![]);
924        let command = Command::new(Some(id.clone()), infolist_buffer);
925        assert_eq!(command.to_string(), "(infolist_buffer) infolist buffer\n");
926
927        let infolist_buffer = InfolistCommand::new(name.clone(), Some(pointer.clone()), vec![]);
928        let command = Command::new(Some(id.clone()), infolist_buffer);
929        assert_eq!(
930            command.to_string(),
931            "(infolist_buffer) infolist buffer 0x1234abcd\n"
932        );
933
934        let infolist_buffer = InfolistCommand::new(name.clone(), None, arguments.clone());
935        let command = Command::new(Some(id.clone()), infolist_buffer);
936        assert_eq!(
937            command.to_string(),
938            "(infolist_buffer) infolist buffer 0x0 core.weechat\n"
939        );
940
941        let infolist_buffer = InfolistCommand::new(name, Some(pointer), arguments);
942        let command = Command::new(Some(id), infolist_buffer);
943        assert_eq!(
944            command.to_string(),
945            "(infolist_buffer) infolist buffer 0x1234abcd core.weechat\n"
946        );
947    }
948
949    #[test]
950    fn test_nicklist() {
951        let all_buffers = NicklistCommand { buffer: None };
952        let one_buffer = NicklistCommand {
953            buffer: Some(PointerOrName::Name("irc.libera.#weechat".to_string())),
954        };
955
956        let command = Command::new(Some("nicklist_all".to_string()), all_buffers);
957        assert_eq!(command.to_string(), "(nicklist_all) nicklist\n");
958
959        let command = Command::new(Some("nicklist_weechat".to_string()), one_buffer);
960        assert_eq!(
961            command.to_string(),
962            "(nicklist_weechat) nicklist irc.libera.#weechat\n"
963        );
964    }
965
966    #[test]
967    fn test_input() {
968        let help = InputCommand {
969            buffer: PointerOrName::Name("core.weechat".to_string()),
970            data: "/help filter".to_string(),
971        };
972
973        let hello = InputCommand {
974            buffer: PointerOrName::Name("irc.libera.#weechat".to_string()),
975            data: "hello!".to_string(),
976        };
977
978        let command = Command::new(None, help);
979        assert_eq!(command.to_string(), "input core.weechat /help filter\n");
980
981        let command = Command::new(None, hello);
982        assert_eq!(command.to_string(), "input irc.libera.#weechat hello!\n");
983    }
984
985    #[test]
986    fn test_completion() {
987        let completion_help = CompletionCommand {
988            buffer: PointerOrName::Name("core.weechat".to_string()),
989            position: None,
990            data: Some("/help fi".to_string()),
991        };
992
993        let completion_query = CompletionCommand {
994            buffer: PointerOrName::Name("core.weechat".to_string()),
995            position: Some(5),
996            data: Some("/quernick".to_string()),
997        };
998
999        let command = Command::new(Some("completion_help".to_string()), completion_help);
1000        assert_eq!(
1001            command.to_string(),
1002            "(completion_help) completion core.weechat -1 /help fi\n"
1003        );
1004
1005        let command = Command::new(Some("completion_query".to_string()), completion_query);
1006        assert_eq!(
1007            command.to_string(),
1008            "(completion_query) completion core.weechat 5 /quernick\n"
1009        );
1010    }
1011
1012    #[test]
1013    fn test_sync() {
1014        let all_buffers = SyncCommand::AllBuffers(SyncAllBuffers {
1015            buffers: true,
1016            upgrade: true,
1017            buffer: true,
1018            nicklist: true,
1019        });
1020
1021        let core_buffer = SyncCommand::SomeBuffers(
1022            vec![PointerOrName::Name("core.buffer".to_string())],
1023            SyncSomeBuffers::All,
1024        );
1025
1026        let without_nicklist = SyncCommand::SomeBuffers(
1027            vec![PointerOrName::Name("irc.libera.#weechat".to_string())],
1028            SyncSomeBuffers::Buffer,
1029        );
1030
1031        let general_signals = SyncCommand::AllBuffers(SyncAllBuffers {
1032            buffers: true,
1033            upgrade: true,
1034            buffer: false,
1035            nicklist: false,
1036        });
1037
1038        let command = Command {
1039            id: None,
1040            command: all_buffers,
1041        };
1042        assert_eq!(command.to_string(), "sync\n");
1043
1044        let command = Command::new(None, core_buffer);
1045        assert_eq!(command.to_string(), "sync core.buffer\n");
1046
1047        let command = Command::new(None, without_nicklist);
1048        assert_eq!(command.to_string(), "sync irc.libera.#weechat buffer\n");
1049
1050        let command = Command::new(None, general_signals);
1051        assert_eq!(command.to_string(), "sync * buffers,upgrade\n");
1052    }
1053
1054    #[test]
1055    fn test_desync() {
1056        let all_buffers = DesyncCommand::AllBuffers(SyncAllBuffers {
1057            buffers: true,
1058            upgrade: true,
1059            buffer: true,
1060            nicklist: true,
1061        });
1062
1063        let nicklist = DesyncCommand::SomeBuffers(
1064            vec![PointerOrName::Name("irc.libera.#weechat".to_string())],
1065            SyncSomeBuffers::Nicklist,
1066        );
1067
1068        let all_signals = DesyncCommand::SomeBuffers(
1069            vec![PointerOrName::Name("irc.libera.#weechat".to_string())],
1070            SyncSomeBuffers::All,
1071        );
1072
1073        let command = Command::new(None, all_buffers);
1074        assert_eq!(command.to_string(), "desync\n");
1075
1076        let command = Command::new(None, nicklist);
1077        assert_eq!(command.to_string(), "desync irc.libera.#weechat nicklist\n");
1078
1079        let command = Command::new(None, all_signals);
1080        assert_eq!(command.to_string(), "desync irc.libera.#weechat\n");
1081    }
1082
1083    #[test]
1084    fn test_test() {
1085        let test = TestCommand {};
1086        let command = Command::new(None, test);
1087        assert_eq!(command.to_string(), "test\n");
1088    }
1089
1090    #[test]
1091    fn test_ping() {
1092        let ping = PingCommand {
1093            argument: "foo".to_string(),
1094        };
1095        let command = Command::new(None, ping);
1096        assert_eq!(command.to_string(), "ping foo\n");
1097    }
1098
1099    #[test]
1100    fn test_quit() {
1101        let quit = QuitCommand {};
1102        let command = Command::new(None, quit);
1103        assert_eq!(command.to_string(), "quit\n");
1104    }
1105}