gix_credentials/protocol/context/
serde.rs1use bstr::BStr;
2
3use crate::protocol::context::Error;
4
5mod write {
6 use bstr::{BStr, BString};
7
8 use crate::protocol::{context::serde::validate, Context};
9
10 impl Context {
11 pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> {
13 use bstr::ByteSlice;
14 fn write_key(out: &mut impl std::io::Write, key: &str, value: &BStr) -> std::io::Result<()> {
15 out.write_all(key.as_bytes())?;
16 out.write_all(b"=")?;
17 out.write_all(value)?;
18 out.write_all(b"\n")
19 }
20 let Context {
21 protocol,
22 host,
23 path,
24 username,
25 password,
26 oauth_refresh_token,
27 password_expiry_utc,
28 url,
29 quit: _,
32 } = self;
33 for (key, value) in [("url", url), ("path", path)] {
34 if let Some(value) = value {
35 validate(key, value.as_slice().into()).map_err(std::io::Error::other)?;
36 write_key(&mut out, key, value.as_ref()).ok();
37 }
38 }
39 for (key, value) in [
40 ("protocol", protocol),
41 ("host", host),
42 ("username", username),
43 ("password", password),
44 ("oauth_refresh_token", oauth_refresh_token),
45 ] {
46 if let Some(value) = value {
47 validate(key, value.as_str().into()).map_err(std::io::Error::other)?;
48 write_key(&mut out, key, value.as_bytes().as_bstr()).ok();
49 }
50 }
51 if let Some(value) = password_expiry_utc {
52 let key = "password_expiry_utc";
53 let value = value.to_string();
54 validate(key, value.as_str().into()).map_err(std::io::Error::other)?;
55 write_key(&mut out, key, value.as_bytes().as_bstr()).ok();
56 }
57 Ok(())
58 }
59
60 pub fn to_bstring(&self) -> BString {
62 let mut buf = Vec::<u8>::new();
63 self.write_to(&mut buf).expect("infallible");
64 buf.into()
65 }
66 }
67}
68
69pub mod decode {
71 use bstr::{BString, ByteSlice};
72
73 use crate::protocol::{context, context::serde::validate, Context};
74
75 #[derive(Debug, thiserror::Error)]
77 #[allow(missing_docs)]
78 pub enum Error {
79 #[error("Illformed UTF-8 in value of key {key:?}: {value:?}")]
80 IllformedUtf8InValue { key: String, value: BString },
81 #[error(transparent)]
82 Encoding(#[from] context::Error),
83 #[error("Invalid format in line {line:?}, expecting key=value")]
84 Syntax { line: BString },
85 }
86
87 impl Context {
88 pub fn from_bytes(input: &[u8]) -> Result<Self, Error> {
90 let mut ctx = Context::default();
91 let Context {
92 protocol,
93 host,
94 path,
95 username,
96 password,
97 oauth_refresh_token,
98 password_expiry_utc,
99 url,
100 quit,
101 } = &mut ctx;
102 for res in input.lines().take_while(|line| !line.is_empty()).map(|line| {
103 let mut it = line.splitn(2, |b| *b == b'=');
104 match (
105 it.next().and_then(|k| k.to_str().ok()),
106 it.next().map(ByteSlice::as_bstr),
107 ) {
108 (Some(key), Some(value)) => validate(key, value)
109 .map(|_| (key, value.to_owned()))
110 .map_err(Into::into),
111 _ => Err(Error::Syntax { line: line.into() }),
112 }
113 }) {
114 let (key, value) = res?;
115 match key {
116 "protocol" | "host" | "username" | "password" | "oauth_refresh_token" => {
117 if !value.is_utf8() {
118 return Err(Error::IllformedUtf8InValue { key: key.into(), value });
119 }
120 let value = value.to_string();
121 *match key {
122 "protocol" => &mut *protocol,
123 "host" => host,
124 "username" => username,
125 "password" => password,
126 "oauth_refresh_token" => oauth_refresh_token,
127 _ => unreachable!("checked field names in match above"),
128 } = Some(value);
129 }
130 "password_expiry_utc" => {
131 *password_expiry_utc = value.to_str().ok().and_then(|value| value.parse().ok());
132 }
133 "url" => *url = Some(value),
134 "path" => *path = Some(value),
135 "quit" => {
136 *quit = gix_config_value::Boolean::try_from(value.as_ref()).ok().map(Into::into);
137 }
138 _ => {}
139 }
140 }
141 Ok(ctx)
142 }
143 }
144}
145
146fn validate(key: &str, value: &BStr) -> Result<(), Error> {
147 if key.contains('\0') || key.contains('\n') || value.contains(&0) || value.contains(&b'\n') {
148 return Err(Error::Encoding {
149 key: key.to_owned(),
150 value: value.to_owned(),
151 });
152 }
153 Ok(())
154}