use std::borrow::Cow;
use http::{HeaderValue, StatusCode, header::LOCATION};
use percent_encoding::{CONTROLS, utf8_percent_encode};
use topcoat_core::{context::Cx, error::Result};
use crate::response::{IntoResponse, Response};
#[must_use]
pub fn redirect(uri: impl AsRef<str>) -> RedirectError {
RedirectError::new(StatusCode::TEMPORARY_REDIRECT, uri.as_ref())
}
#[must_use]
pub fn redirect_permanent(uri: impl AsRef<str>) -> RedirectError {
RedirectError::new(StatusCode::PERMANENT_REDIRECT, uri.as_ref())
}
#[derive(Debug)]
pub struct RedirectError {
status: StatusCode,
location: HeaderValue,
}
impl RedirectError {
fn new(status: StatusCode, uri: &str) -> Self {
Self {
status,
location: location(uri),
}
}
pub(crate) fn location(&self) -> &HeaderValue {
&self.location
}
}
impl std::fmt::Display for RedirectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("redirect")
}
}
impl std::error::Error for RedirectError {}
impl IntoResponse for RedirectError {
fn into_response(self, cx: &Cx) -> Result<Response> {
(self.status, ([(LOCATION, self.location)], ())).into_response(cx)
}
}
#[must_use]
pub fn see_other(uri: impl AsRef<str>) -> SeeOther {
SeeOther::new(uri.as_ref())
}
#[derive(Debug)]
pub struct SeeOther {
location: HeaderValue,
}
impl SeeOther {
fn new(uri: &str) -> Self {
Self {
location: location(uri),
}
}
}
impl IntoResponse for SeeOther {
fn into_response(self, cx: &Cx) -> Result<Response> {
(StatusCode::SEE_OTHER, ([(LOCATION, self.location)], ())).into_response(cx)
}
}
fn location(uri: &str) -> HeaderValue {
let uri: Cow<'_, str> = utf8_percent_encode(uri, CONTROLS).into();
HeaderValue::try_from(&*uri).expect("percent-encoded uri is a valid header value")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_targets_are_kept_as_is() {
assert_eq!(
redirect("/users?page=2#top").location(),
"/users?page=2#top"
);
}
#[test]
fn non_ascii_targets_are_percent_encoded() {
assert_eq!(redirect("/caf\u{e9}").location(), "/caf%C3%A9");
assert_eq!(
redirect_permanent("https://\u{4f8b}\u{3048}.jp/").location(),
"https://%E4%BE%8B%E3%81%88.jp/"
);
assert_eq!(see_other("/caf\u{e9}").location, "/caf%C3%A9");
}
#[test]
fn control_characters_are_percent_encoded() {
assert_eq!(redirect("/a\r\nb\x7f").location(), "/a%0D%0Ab%7F");
}
#[test]
fn encoded_targets_are_not_encoded_twice() {
assert_eq!(
redirect("/caf%C3%A9?q=a%20b").location(),
"/caf%C3%A9?q=a%20b"
);
}
#[test]
fn the_location_converts_back_to_a_str() {
assert!(redirect("/caf\u{e9} \u{4f8b}").location().to_str().is_ok());
}
}