ssh_cli/domain/
command.rs1#![forbid(unsafe_code)]
4
5use super::error::DomainError;
6use super::limits::CharLimit;
7use crate::errors::{SshCliError, SshCliResult};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use std::fmt;
10use std::path::{Path, PathBuf};
11
12#[repr(transparent)]
14#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
15pub struct RemoteCommand(String);
16
17impl RemoteCommand {
18 pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
20 let s = raw.as_ref();
21 if s.trim().is_empty() {
22 return Err(DomainError::new("remote_command", "empty command"));
23 }
24 if s.as_bytes().contains(&0) {
25 return Err(DomainError::new(
26 "remote_command",
27 "command contains null byte",
28 ));
29 }
30 Ok(Self(s.to_owned()))
31 }
32
33 #[must_use]
35 pub fn as_str(&self) -> &str {
36 &self.0
37 }
38
39 #[must_use]
41 pub fn into_inner(self) -> String {
42 self.0
43 }
44
45 pub fn check_length(&self, max: CharLimit) -> SshCliResult<()> {
47 let lim = max.effective();
48 let len = self.0.chars().count();
49 if len > lim {
50 return Err(SshCliError::CommandTooLong {
51 max: max.wire(),
52 len,
53 });
54 }
55 Ok(())
56 }
57}
58
59impl AsRef<str> for RemoteCommand {
60 fn as_ref(&self) -> &str {
61 self.as_str()
62 }
63}
64
65impl fmt::Display for RemoteCommand {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str(self.as_str())
68 }
69}
70
71impl<'de> Deserialize<'de> for RemoteCommand {
72 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
73 let s = String::deserialize(d)?;
74 Self::try_new(s).map_err(serde::de::Error::custom)
75 }
76}
77
78#[repr(transparent)]
80#[derive(Clone, Debug, PartialEq, Eq, Hash)]
81pub struct KeyPath(PathBuf);
82
83impl KeyPath {
84 pub fn try_new(raw: impl AsRef<Path>) -> Result<Self, DomainError> {
86 let p = raw.as_ref();
87 let s = p.to_string_lossy();
88 if s.trim().is_empty() {
89 return Err(DomainError::new("key_path", "key path must not be empty"));
90 }
91 Ok(Self(p.to_path_buf()))
92 }
93
94 pub fn try_from_optional(raw: Option<impl AsRef<str>>) -> Result<Option<Self>, DomainError> {
96 match raw {
97 None => Ok(None),
98 Some(s) => {
99 let t = s.as_ref().trim();
100 if t.is_empty() {
101 Ok(None)
102 } else {
103 Self::try_new(t).map(Some)
104 }
105 }
106 }
107 }
108
109 #[must_use]
111 pub fn as_path(&self) -> &Path {
112 &self.0
113 }
114
115 #[must_use]
117 pub fn to_string_lossy_owned(&self) -> String {
118 self.0.to_string_lossy().into_owned()
119 }
120
121 #[must_use]
123 pub fn into_path_buf(self) -> PathBuf {
124 self.0
125 }
126}
127
128impl AsRef<Path> for KeyPath {
129 fn as_ref(&self) -> &Path {
130 self.as_path()
131 }
132}
133
134impl fmt::Display for KeyPath {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "{}", self.0.display())
137 }
138}
139
140impl Serialize for KeyPath {
141 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
142 s.serialize_str(&self.to_string_lossy_owned())
143 }
144}
145
146impl<'de> Deserialize<'de> for KeyPath {
147 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
148 let s = String::deserialize(d)?;
149 Self::try_new(s).map_err(serde::de::Error::custom)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn remote_command_and_key() {
159 assert!(RemoteCommand::try_new(" ").is_err());
160 assert!(RemoteCommand::try_new("echo\0x").is_err());
161 assert!(RemoteCommand::try_new("uptime").is_ok());
162 assert!(KeyPath::try_new("").is_err());
163 assert!(KeyPath::try_new("/home/u/.ssh/id_ed25519").is_ok());
164 assert!(KeyPath::try_from_optional(Some("")).unwrap().is_none());
165 }
166}