1use std::{fmt, str::FromStr};
4
5use bon::Builder;
6use const_macros::const_early;
7
8use miette::Diagnostic;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13use thiserror::Error;
14
15use crate::{
16 auth::{
17 part::{self, Part, SEPARATOR},
18 query::Query,
19 url::{self, Url},
20 utf8,
21 },
22 macros::errors,
23};
24
25#[derive(Debug, Error, Diagnostic)]
27#[error("empty label encountered")]
28#[diagnostic(
29 code(otp_std::auth::label::empty),
30 help("make sure the label is non-empty")
31)]
32pub struct EmptyError;
33
34#[derive(Debug, Error, Diagnostic)]
36#[error(transparent)]
37#[diagnostic(transparent)]
38pub enum ParseErrorSource {
39 Empty(#[from] EmptyError),
41 Part(#[from] part::Error),
43}
44
45#[derive(Debug, Error, Diagnostic)]
47#[error("failed to parse label")]
48#[diagnostic(
49 code(otp_std::auth::label),
50 help("make sure the label is formatted correctly")
51)]
52pub struct ParseError {
53 #[source]
55 #[diagnostic_source]
56 pub source: ParseErrorSource,
57}
58
59impl ParseError {
60 pub const fn new(source: ParseErrorSource) -> Self {
62 Self { source }
63 }
64
65 pub fn empty(error: EmptyError) -> Self {
67 Self::new(error.into())
68 }
69
70 pub fn part(error: part::Error) -> Self {
72 Self::new(error.into())
73 }
74
75 pub fn new_empty() -> Self {
77 Self::empty(EmptyError)
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Builder)]
83#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
84pub struct Label<'l> {
85 pub issuer: Option<Part<'l>>,
87 pub user: Part<'l>,
89}
90
91pub type Parts<'p> = (Option<Part<'p>>, Part<'p>);
93
94pub type OwnedParts = Parts<'static>;
96
97impl<'l> Label<'l> {
98 pub fn from_parts(parts: Parts<'l>) -> Self {
100 let (issuer, user) = parts;
101
102 Self::builder().maybe_issuer(issuer).user(user).build()
103 }
104
105 pub fn into_parts(self) -> Parts<'l> {
107 (self.issuer, self.user)
108 }
109}
110
111impl<'p> From<Parts<'p>> for Label<'p> {
112 fn from(parts: Parts<'p>) -> Self {
113 Self::from_parts(parts)
114 }
115}
116
117impl<'l> From<Label<'l>> for Parts<'l> {
118 fn from(label: Label<'l>) -> Self {
119 label.into_parts()
120 }
121}
122
123impl fmt::Display for Label<'_> {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 if let Some(issuer) = self.issuer.as_ref() {
126 issuer.fmt(formatter)?;
127
128 formatter.write_str(SEPARATOR)?;
129 };
130
131 self.user.fmt(formatter)
132 }
133}
134
135errors! {
136 Type = ParseError,
137 Hack = $,
138 empty_error => new_empty(),
139}
140
141impl FromStr for Label<'_> {
142 type Err = ParseError;
143
144 fn from_str(string: &str) -> Result<Self, Self::Err> {
145 const_early!(string.is_empty() => empty_error!());
146
147 if let Some((issuer_string, user_string)) = string.split_once(SEPARATOR) {
148 let issuer = issuer_string.parse().map_err(Self::Err::part)?;
149 let user = user_string.parse().map_err(Self::Err::part)?;
150
151 Ok(Self::builder().issuer(issuer).user(user).build())
152 } else {
153 let user = string.parse().map_err(Self::Err::part)?;
154
155 Ok(Self::builder().user(user).build())
156 }
157 }
158}
159
160#[derive(Debug, Error, Diagnostic)]
162#[error(transparent)]
163#[diagnostic(transparent)]
164pub enum DecodeErrorSource {
165 Utf8(#[from] utf8::Error),
167 Parse(#[from] ParseError),
169}
170
171#[derive(Debug, Error, Diagnostic)]
173#[error("failed to decode label")]
174#[diagnostic(
175 code(otp_std::auth::label::decode),
176 help("make sure the label is correctly formatted")
177)]
178pub struct DecodeError {
179 #[source]
181 #[diagnostic_source]
182 pub source: DecodeErrorSource,
183}
184
185impl DecodeError {
186 pub const fn new(source: DecodeErrorSource) -> Self {
188 Self { source }
189 }
190
191 pub fn utf8(error: utf8::Error) -> Self {
193 Self::new(error.into())
194 }
195
196 pub fn label(error: ParseError) -> Self {
198 Self::new(error.into())
199 }
200}
201
202impl Label<'_> {
203 pub fn decode<S: AsRef<str>>(string: S) -> Result<Self, DecodeError> {
209 let string = string.as_ref();
210
211 let decoded = url::decode(string)
212 .map_err(utf8::wrap)
213 .map_err(DecodeError::utf8)?;
214
215 decoded.parse().map_err(DecodeError::label)
216 }
217}
218
219impl Label<'_> {
220 pub fn encode(&self) -> String {
222 self.to_string()
223 }
224}
225
226#[derive(Debug, Error, Diagnostic)]
228#[error("issuer mismatch: `{label}` in label, `{query}` in query")]
229#[diagnostic(
230 code(otp_std::auth::label::mismatch),
231 help("if the issuer is present both in the label and the query, they must match")
232)]
233pub struct MismatchError {
234 pub label: String,
236 pub query: String,
238}
239
240impl MismatchError {
241 pub const fn new(label: String, query: String) -> Self {
243 Self { label, query }
244 }
245}
246
247errors! {
248 Type = MismatchError,
249 Hack = $,
250 mismatch_error => new(label => into_owned, query => into_owned),
251}
252
253pub fn try_match<'p>(
261 label_issuer: Option<Part<'p>>,
262 query_issuer: Option<Part<'p>>,
263) -> Result<Option<Part<'p>>, MismatchError> {
264 match (label_issuer, query_issuer) {
265 (Some(label), Some(query)) if label != query => {
266 Err(mismatch_error!(label.get(), query.get()))
267 }
268 (label_option, query_option) => Ok(label_option.or(query_option)),
269 }
270}
271
272#[derive(Debug, Error, Diagnostic)]
274#[error(transparent)]
275#[diagnostic(transparent)]
276pub enum ErrorSource {
277 Decode(#[from] DecodeError),
279 Issuer(#[from] part::DecodeError),
281 Mismatch(#[from] MismatchError),
283}
284
285#[derive(Debug, Error, Diagnostic)]
287#[error("failed to extract label from OTP URL")]
288#[diagnostic(
289 code(otp_std::auth::label),
290 help("see the report for more information")
291)]
292pub struct Error {
293 #[source]
295 #[diagnostic_source]
296 pub source: ErrorSource,
297}
298
299impl Error {
300 pub const fn new(source: ErrorSource) -> Self {
302 Self { source }
303 }
304
305 pub fn decode(error: DecodeError) -> Self {
307 Self::new(error.into())
308 }
309
310 pub fn mismatch(error: MismatchError) -> Self {
312 Self::new(error.into())
313 }
314
315 pub fn issuer(error: part::DecodeError) -> Self {
317 Self::new(error.into())
318 }
319}
320
321pub const ISSUER: &str = "issuer";
323
324pub const SLASH: &str = "/";
326
327impl Label<'_> {
328 pub fn query_for(&self, url: &mut Url) {
330 if let Some(issuer) = self.issuer.as_ref() {
331 url.query_pairs_mut()
332 .append_pair(ISSUER, issuer.encode().as_ref());
333 };
334 }
335
336 pub fn extract_from(query: &mut Query<'_>, url: &Url) -> Result<Self, Error> {
342 let path = url.path().trim_start_matches(SLASH);
343
344 let label = Self::decode(path).map_err(Error::decode)?;
345
346 let (label_issuer, user) = label.into_parts();
348
349 let query_issuer = query
350 .remove(ISSUER)
351 .map(Part::decode)
352 .transpose()
353 .map_err(Error::issuer)?;
354
355 let issuer = try_match(label_issuer, query_issuer).map_err(Error::mismatch)?;
356
357 Ok(Self::from_parts((issuer, user)))
358 }
359}
360
361pub type Owned = Label<'static>;
363
364impl Label<'_> {
365 pub fn into_owned(self) -> Owned {
367 Owned::builder()
368 .maybe_issuer(self.issuer.map(Part::into_owned))
369 .user(self.user.into_owned())
370 .build()
371 }
372}