ap: refactor CachedLoader

This commit is contained in:
anna 2023-01-25 21:54:01 +01:00
parent 78fbbdfedd
commit bda3a9027d
Signed by: fef
GPG key ID: EC22E476DC2D3D84
8 changed files with 191 additions and 248 deletions

View file

@ -1,111 +1,35 @@
// This file is an adaptation of
// This file was originally based on
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/content_type.rs>.
//
// Copyright (C) 2022 Timothée Haudebourg et al.
// Licensed under either the Apache License, Version 2.0; or the MIT License.
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
//
// Modified by anna <owo@fef.moe> to accept the `application/activity+json`
// MIME type as proper JSON-LD.
// Heavily modified by anna <owo@fef.moe> to use nyanoblog's own Structured Header
// parser and accept the `application/activity+json` MIME type as proper JSON-LD.
use hashbrown::HashMap;
use mime::Mime;
use reqwest::header::HeaderValue;
use std::str::FromStr;
use crate::util::header::{Item, ParseHeader};
/// Helper structure for parsing the `Content-Type` header for JSON-LD.
pub struct ContentType {
mime: Mime,
params: HashMap<Vec<u8>, Vec<u8>>,
profile: Option<String>,
}
impl ContentType {
pub fn from_header(value: &HeaderValue) -> Option<ContentType> {
enum State {
Mime,
NextParam,
BeginKey,
Key,
BeginValue,
QuotedValue,
Value,
}
let mut state = State::Mime;
let mut mime = Vec::new();
let mut current_key = Vec::new();
let mut current_value = Vec::new();
let mut params = HashMap::new();
let mut bytes = value.as_bytes().iter();
loop {
match state {
State::Mime => match bytes.next().copied() {
Some(b';') => state = State::BeginKey,
Some(b) => mime.push(b),
None => break,
},
State::NextParam => match bytes.next().copied() {
Some(b';') => state = State::BeginKey,
Some(_) => return None,
None => break,
},
State::BeginKey => match bytes.next().copied() {
Some(b' ') => {}
Some(b) => {
current_key.push(b);
state = State::Key;
}
None => return None,
},
State::Key => match bytes.next().copied() {
Some(b'=') => state = State::BeginValue,
Some(b) => current_key.push(b),
None => return None,
},
State::BeginValue => match bytes.next().copied() {
Some(b'"') => state = State::QuotedValue,
Some(b) => {
state = State::Value;
current_value.push(b);
}
_ => return None,
},
State::QuotedValue => match bytes.next().copied() {
Some(b'"') => {
params.insert(
std::mem::take(&mut current_key),
std::mem::take(&mut current_value),
);
state = State::NextParam;
}
Some(b) => current_value.push(b),
None => return None,
},
State::Value => match bytes.next().copied() {
Some(b';') => {
params.insert(
std::mem::take(&mut current_key),
std::mem::take(&mut current_value),
);
state = State::BeginKey;
}
Some(b) => current_value.push(b),
None => {
params.insert(
std::mem::take(&mut current_key),
std::mem::take(&mut current_value),
);
break;
}
},
}
}
Mime::from_str(std::str::from_utf8(&mime).ok()?)
.map(|mime| ContentType { mime, params })
.ok()
let item = Item::parse_from_header(value, true).ok()?;
let mime = Mime::from_str(item.as_token()?).ok()?;
let profile = item.param("profile").and_then(|profile| {
profile
.as_string()
.or_else(|| profile.as_token().map(String::from))
});
Some(ContentType { mime, profile })
}
pub fn is_json_ld(&self) -> bool {
@ -114,14 +38,11 @@ impl ContentType {
"application/ld+json",
"application/json",
]
.iter()
.any(|mime| *mime == self.mime)
.contains(&self.mime.as_ref())
}
pub fn is_proper_json_ld(&self) -> bool {
["application/activity+json", "application/ld+json"]
.iter()
.any(|mime| *mime == self.mime)
["application/activity+json", "application/ld+json"].contains(&self.mime.as_ref())
}
pub fn mime(&self) -> &Mime {
@ -132,8 +53,8 @@ impl ContentType {
self.mime
}
pub fn profile(&self) -> Option<&[u8]> {
self.params.get(b"profile".as_slice()).map(Vec::as_slice)
pub fn profile(&self) -> Option<&str> {
self.profile.as_deref()
}
}
@ -153,7 +74,7 @@ mod tests {
assert_eq!(*content_type.mime(), "application/ld+json");
assert_eq!(
content_type.profile(),
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
Some("http://www.w3.org/ns/json-ld#expanded")
);
}
@ -169,7 +90,7 @@ mod tests {
assert_eq!(*content_type.mime(), "application/ld+json");
assert_eq!(
content_type.profile(),
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
Some("http://www.w3.org/ns/json-ld#expanded")
);
}
@ -185,7 +106,7 @@ mod tests {
assert_eq!(*content_type.mime(), "application/ld+json");
assert_eq!(
content_type.profile(),
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
Some("http://www.w3.org/ns/json-ld#expanded")
);
}
@ -201,7 +122,7 @@ mod tests {
assert_eq!(*content_type.mime(), "application/ld+json");
assert_eq!(
content_type.profile(),
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
Some("http://www.w3.org/ns/json-ld#expanded")
);
}
@ -217,7 +138,7 @@ mod tests {
assert_eq!(*content_type.mime(), "application/ld+json");
assert_eq!(
content_type.profile(),
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
Some("http://www.w3.org/ns/json-ld#expanded")
);
}
@ -233,7 +154,7 @@ mod tests {
assert_eq!(*content_type.mime(), "application/ld+json");
assert_eq!(
content_type.profile(),
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
Some("http://www.w3.org/ns/json-ld#expanded")
);
}
@ -243,10 +164,7 @@ mod tests {
assert_eq!(*content_type.mime(), "application/ld+json");
assert_eq!(
content_type.profile(),
Some(
b"http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted"
.as_slice()
)
Some("http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted")
);
}

View file

@ -1,110 +1,49 @@
// This file is an adaptation of
// This file was originally based on
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/link.rs>.
//
// Copyright (C) 2022 Timothée Haudebourg et al.
// Licensed under either the Apache License, Version 2.0; or the MIT License.
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
//
// Heavily modified by anna <owo@fef.moe> to use nyanoblog's own Structured Header
// parser and accept the `application/activity+json` MIME type as proper JSON-LD.
use iref::{IriRef, IriRefBuf};
use reqwest::header::HeaderValue;
use std::collections::HashMap;
use std::str::FromStr;
use crate::util::header::{Item, ParseHeader};
pub struct Link {
href: IriRefBuf,
params: HashMap<Vec<u8>, Vec<u8>>,
rel: Option<String>,
typ: Option<String>,
}
impl Link {
pub fn from_header(value: &HeaderValue) -> Option<Self> {
enum State {
BeginHref,
Href,
NextParam,
BeginKey,
Key,
BeginValue,
Value,
}
let mut state = State::BeginHref;
let mut href = Vec::new();
let mut current_key = Vec::new();
let mut current_value = Vec::new();
let mut params = HashMap::new();
let mut bytes = value.as_bytes().iter();
loop {
match state {
State::BeginHref => match bytes.next().copied() {
Some(b'<') => state = State::Href,
_ => break None,
},
State::Href => match bytes.next().copied() {
Some(b'>') => state = State::NextParam,
Some(b) => {
href.push(b);
}
None => break None,
},
State::NextParam => match bytes.next().copied() {
Some(b';') => state = State::BeginKey,
Some(_) => break None,
None => {
break match IriRefBuf::from_vec(href) {
Ok(href) => Some(Self { href, params }),
Err(_) => None,
}
}
},
State::BeginKey => match bytes.next().copied() {
Some(b' ') => {}
Some(b) => {
current_key.push(b);
state = State::Key
}
None => break None,
},
State::Key => match bytes.next().copied() {
Some(b'=') => state = State::BeginValue,
Some(b) => current_key.push(b),
None => break None,
},
State::BeginValue => match bytes.next().copied() {
Some(b'"') => state = State::Value,
_ => break None,
},
State::Value => match bytes.next().copied() {
Some(b'"') => {
params.insert(
std::mem::take(&mut current_key),
std::mem::take(&mut current_value),
);
state = State::NextParam
}
Some(b) => current_value.push(b),
None => break None,
},
}
}
let item = Item::parse_from_header(value, false).unwrap();
let href = IriRefBuf::from_str(item.as_url()?).ok()?;
let rel = item.param("rel").and_then(|rel| rel.as_string());
let typ = item.param("type").and_then(|typ| typ.as_string());
Some(Link { href, rel, typ })
}
pub fn href(&self) -> IriRef {
self.href.as_iri_ref()
}
pub fn rel(&self) -> Option<&[u8]> {
self.params.get(b"rel".as_slice()).map(Vec::as_slice)
pub fn rel(&self) -> Option<&str> {
self.rel.as_deref()
}
pub fn typ(&self) -> Option<&[u8]> {
self.params.get(b"type".as_slice()).map(Vec::as_slice)
pub fn typ(&self) -> Option<&str> {
self.typ.as_deref()
}
pub fn is_proper_json_ld(&self) -> bool {
self.typ()
.map(|typ| typ == b"application/activity+json" || typ == b"application/ld+json")
.map(|typ| ["application/activity+json", "application/ld+json"].contains(&typ))
.unwrap_or(false)
}
}
@ -123,16 +62,16 @@ mod tests {
)
.unwrap();
assert_eq!(link.href(), "http://www.example.org/context");
assert_eq!(link.rel(), Some(b"context".as_slice()));
assert_eq!(link.typ(), Some(b"application/ld+json".as_slice()))
assert_eq!(link.rel(), Some("context"));
assert_eq!(link.typ(), Some("application/ld+json"))
}
#[test]
fn parse_link_2() {
let link = Link::from_header(&HeaderValue::from_str("<http://www.example.org/context>; rel=\"context\"; type=\"application/ld+json\"; foo=\"bar\"").unwrap()).unwrap();
assert_eq!(link.href(), "http://www.example.org/context");
assert_eq!(link.rel(), Some(b"context".as_slice()));
assert_eq!(link.typ(), Some(b"application/ld+json".as_slice()))
assert_eq!(link.rel(), Some("context"));
assert_eq!(link.typ(), Some("application/ld+json"))
}
#[test]

View file

@ -13,18 +13,19 @@
use crate::state::AppState;
use bytes::Bytes;
use futures::future::{BoxFuture, FutureExt};
use hashbrown::{HashMap, HashSet};
use hashbrown::HashSet;
use iref::{Iri, IriBuf};
use json_ld::{syntax::Parse, Loader, LoadingResult, Profile, RemoteDocument, Value};
use json_ld::{
syntax::{Parse, Value},
Loader, Profile, RemoteDocument,
};
use locspan::{Meta, Span};
use mime::Mime;
use rdf_types::vocabulary::Index;
use rdf_types::{IriVocabulary, IriVocabularyMut};
use reqwest::header::{HeaderValue, ACCEPT, CONTENT_TYPE, LINK, LOCATION};
use reqwest::StatusCode;
use reqwest::{
header::{ACCEPT, CONTENT_TYPE, LINK, LOCATION},
StatusCode,
};
use std::hash::Hash;
use std::str::FromStr;
use tokio::sync::OnceCell;
mod content_type;
use content_type::*;
@ -34,8 +35,9 @@ use link::*;
use crate::core::*;
use crate::headers;
use crate::util::http;
use crate::util::http::Response;
pub struct CachedLoader<I = IriBuf, M = Span, T = json_ld::syntax::Value<M>> {
pub struct CachedLoader<I = IriBuf, M = Span, T = Value<M>> {
state: AppState,
parser: Box<DynParser<I, M, T>>,
}
@ -94,7 +96,7 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
mut url: I,
) -> Result<RemoteDocument<I, M, T>> {
const MAX_REDIRECTS: u32 = 8;
const ACCEPT_ACTIVITY_PUB: &'static str =
const ACCEPT_ACTIVITY_PUB: &str =
"application/activity+json, application/ld+json, application/json";
let mut redirect_count = 0;
@ -129,18 +131,16 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
context_url = self.get_context_url(vocabulary, &url, &response)?;
}
let mut profile = HashSet::new();
for p in content_type
let profile: HashSet<Profile<I>> = content_type
.profile()
.into_iter()
.flat_map(|p| p.split(|b| *b == b' '))
{
if let Ok(iri) = Iri::new(p) {
profile.insert(Profile::new(iri, vocabulary));
}
}
.flat_map(|profile| profile.split(' '))
.filter_map(|p| {
Iri::new(p).ok().map(|iri| Profile::new(iri, vocabulary))
})
.collect();
let bytes = response.bytes().await.map_err(|e| Error::Reqwest(e))?;
let bytes = response.bytes().await?;
let document = (*self.parser)(vocabulary, &url, bytes)
.map_err(|e| Error::MalformedApub(e.to_string()))?;
@ -156,8 +156,7 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
debug!(target: "ap", "no valid media type found");
for link in response.headers().get_all(LINK).into_iter() {
if let Some(link) = Link::from_header(link) {
if link.rel() == Some(b"alternate") && link.is_proper_json_ld()
{
if link.rel() == Some("alternate") && link.is_proper_json_ld() {
debug!(target: "ap", "link found");
let u = link.href().resolved(vocabulary.iri(&url).unwrap());
url = vocabulary.insert(u.as_iri());
@ -204,13 +203,13 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
&self,
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
url: &I,
response: &reqwest::Response,
response: &Response,
) -> Result<Option<I>> {
let mut context_url = None;
for link in response.headers().get_all(LINK).into_iter() {
if let Some(link) = Link::from_header(link) {
if link.rel() == Some(b"alternate")
&& link.typ() == Some(b"https://www.w3.org/ns/json-ld#context")
if link.rel() == Some("alternate")
&& link.typ() == Some("https://www.w3.org/ns/json-ld#context")
{
if context_url.is_some() {
return Err(Error::MalformedApub(String::from(

View file

@ -14,6 +14,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub enum Error {
BadBearcap,
BadCredentials,
BadHeader(String),
BadRequest,
BadSignature,
BadToken(jsonwebtoken::errors::Error),
@ -33,6 +34,7 @@ impl ResponseError for Error {
match self {
Error::BadBearcap => StatusCode::UNPROCESSABLE_ENTITY,
Error::BadCredentials => StatusCode::UNAUTHORIZED,
Error::BadHeader(_) => StatusCode::BAD_REQUEST,
Error::BadRequest => StatusCode::BAD_REQUEST,
Error::BadSignature => StatusCode::UNAUTHORIZED,
Error::BadToken(_) => StatusCode::UNAUTHORIZED,
@ -55,6 +57,7 @@ impl fmt::Display for Error {
match self {
Error::BadBearcap => write!(f, "Invalid bearcap URL"),
Error::BadCredentials => write!(f, "Invalid user name or password"),
Error::BadHeader(name) => write!(f, "Value for header \"{name}\" is invalid"),
Error::BadRequest => write!(f, "Bad request"),
Error::BadSignature => write!(f, "Bad signature"),
Error::BadToken(jwt_error) => jwt_error.fmt(f),

View file

@ -6,11 +6,15 @@ use crate::core::*;
use crate::util::slice::SliceCursor;
use crate::util::transcode;
/// Parse an HTTP Structured Field Value according to
/// [RFC 8941](https://www.rfc-editor.org/info/rfc8941).
/// Note: This only parses one "line" although the RFC says conforming
/// software MUST support values split over several headers.
pub trait ParseHeader<'a>: Sized {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self>;
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self>;
fn parse_from_header(header: &'a HeaderValue) -> Result<Self> {
Self::parse_from_ascii(header.as_bytes())
fn parse_from_header(header: &'a HeaderValue, strict: bool) -> Result<Self> {
Self::parse_from_ascii(header.as_bytes(), strict)
}
}
@ -44,6 +48,7 @@ pub enum BareItem<'a> {
Decimal(f32),
String(StringItem<'a>),
Token(TokenItem<'a>),
Url(UrlItem<'a>),
ByteSequence(ByteSequenceItem<'a>),
Boolean(bool),
}
@ -53,11 +58,13 @@ pub struct StringItem<'a>(&'a str);
#[derive(Debug, PartialEq)]
pub struct TokenItem<'a>(&'a str);
#[derive(Debug, PartialEq)]
pub struct UrlItem<'a>(&'a str);
#[derive(Debug, PartialEq)]
pub struct ByteSequenceItem<'a>(&'a str);
impl<'a> ParseHeader<'a> for Dictionary<'a> {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
Parser::new(header)?.parse_dictionary()
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
Parser::new(header, strict)?.parse_dictionary()
}
}
@ -72,8 +79,8 @@ impl<'a> Dictionary<'a> {
}
impl<'a> ParseHeader<'a> for List<'a> {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
Parser::new(header)?.parse_list()
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
Parser::new(header, strict)?.parse_list()
}
}
@ -92,12 +99,16 @@ impl<'a> List<'a> {
}
impl<'a> ParseHeader<'a> for Item<'a> {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
Parser::new(header)?.parse_item()
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
Parser::new(header, strict)?.parse_item(!strict)
}
}
impl<'a> Item<'a> {
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
self.params.as_slice()
}
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
where
K: Into<&'a str>,
@ -132,6 +143,10 @@ impl<'a> Item<'a> {
self.bare_item.as_token()
}
pub fn as_url(&self) -> Option<&'a str> {
self.bare_item.as_url()
}
pub fn as_byte_sequence(&self) -> Option<Bytes> {
self.bare_item.as_byte_sequence()
}
@ -170,6 +185,13 @@ impl<'a> BareItem<'a> {
}
}
pub fn as_url(&self) -> Option<&'a str> {
match self {
BareItem::Url(u) => Some(u.0),
_ => None,
}
}
pub fn as_byte_sequence(&self) -> Option<Bytes> {
match self {
BareItem::ByteSequence(bs) => Some(bs.into()),
@ -186,6 +208,13 @@ impl<'a> BareItem<'a> {
}
impl<'a> Member<'a> {
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
match self {
Member::Item(i) => i.get_params(),
Member::InnerList(l) => l.get_params(),
}
}
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
where
K: Into<&'a str>,
@ -269,6 +298,10 @@ impl<'a> InnerList<'a> {
self.items.len()
}
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
self.params.as_slice()
}
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
where
K: Into<&'a str>,
@ -290,13 +323,15 @@ impl<'a> InnerList<'a> {
struct Parser<'a> {
cursor: SliceCursor<'a, u8>,
strict: bool,
}
impl<'a> Parser<'a> {
fn new(data: &'a [u8]) -> Result<Parser> {
if data.is_ascii() {
fn new(data: &'a [u8], strict: bool) -> Result<Parser> {
if data.is_ascii() || (std::str::from_utf8(data).is_ok() && !strict) {
Ok(Parser {
cursor: SliceCursor::new(data),
strict,
})
} else {
Err(Error::BadHeader(String::from(
@ -335,7 +370,7 @@ impl<'a> Parser<'a> {
if self.cursor.peek().copied() == Some(b'(') {
self.parse_inner_list().map(Member::InnerList)
} else {
self.parse_item().map(Member::Item)
self.parse_item(false).map(Member::Item)
}
}
@ -377,7 +412,7 @@ impl<'a> Parser<'a> {
if self.cursor.peek().copied() == Some(b'(') {
Member::InnerList(self.parse_inner_list()?)
} else {
Member::Item(self.parse_item()?)
Member::Item(self.parse_item(false)?)
}
} else {
// parameters
@ -403,7 +438,7 @@ impl<'a> Parser<'a> {
if self.skip_if(|c| c == b')') {
break;
}
items.push(self.parse_item()?);
items.push(self.parse_item(false)?);
// > Parsers MUST support Inner Lists containing at least 256 members.
if items.len() == 256 {
break;
@ -424,8 +459,8 @@ impl<'a> Parser<'a> {
/// ```notrust
/// sf-item = bare-item parameters
/// ```
fn parse_item(&mut self) -> Result<Item<'a>> {
let bare_item = self.parse_bare_item()?;
fn parse_item(&mut self, allow_url: bool) -> Result<Item<'a>> {
let bare_item = self.parse_bare_item(allow_url)?;
let params = self.parse_parameters()?;
Ok(Item { bare_item, params })
}
@ -457,7 +492,7 @@ impl<'a> Parser<'a> {
fn parse_parameter(&mut self) -> Result<(&'a str, BareItem<'a>)> {
let key = self.parse_key()?;
let value = if self.skip_if(|c| c == b'=') {
self.parse_bare_item()?
self.parse_bare_item(false)?
} else {
BareItem::Boolean(true)
};
@ -484,7 +519,7 @@ impl<'a> Parser<'a> {
/// bare-item = sf-integer / sf-decimal / sf-string
/// / sf-token / sf-binary / sf-boolean
/// ```
fn parse_bare_item(&mut self) -> Result<BareItem<'a>> {
fn parse_bare_item(&mut self, allow_url: bool) -> Result<BareItem<'a>> {
match self
.cursor
.peek()
@ -493,10 +528,11 @@ impl<'a> Parser<'a> {
{
c if is_numeric_start(c) => self.parse_numeric(),
b'"' => self.parse_string(),
b'<' if allow_url => self.parse_url(),
c if is_token_start(c) => self.parse_token(),
b':' => self.parse_byte_sequence(),
b'?' => self.parse_boolean(),
_ => Err(self.make_error("Unexpected character")),
c => Err(self.make_error(format!("Unexpected character {:?}", c as char))),
}
}
@ -550,6 +586,21 @@ impl<'a> Parser<'a> {
Ok(BareItem::String(StringItem(slice)))
}
fn parse_url(&mut self) -> Result<BareItem<'a>> {
if self.strict {
return Err(
self.make_error("URLs enclosed in <angle brackets> are forbidden in strict mode")
);
}
self.assert_next(|c| c == b'<')?;
self.chop();
self.skip_while(|c| c != b'>');
let slice = self.chop();
self.assert_next(|c| c == b'>')?;
Ok(BareItem::Url(UrlItem(slice)))
}
/// Parse a Token item (section 3.3.4).
///
/// ```notrust
@ -736,15 +787,15 @@ mod tests {
use crate::util::transcode::base64_decode;
fn mklist(header: &'static str) -> Result<List<'static>> {
List::parse_from_ascii(header.as_bytes())
List::parse_from_ascii(header.as_bytes(), true)
}
fn mkdict(header: &'static str) -> Result<Dictionary<'static>> {
Dictionary::parse_from_ascii(header.as_bytes())
Dictionary::parse_from_ascii(header.as_bytes(), true)
}
fn mkitem(header: &'static str) -> Result<Item<'static>> {
Item::parse_from_ascii(header.as_bytes())
Item::parse_from_ascii(header.as_bytes(), true)
}
#[test]

View file

@ -1,10 +1,15 @@
use reqwest::header::{HeaderName, AUTHORIZATION, USER_AGENT};
use reqwest::{RequestBuilder, Response};
use bytes::Bytes;
use reqwest::header::{HeaderMap, HeaderName, AUTHORIZATION, USER_AGENT};
use reqwest::{RequestBuilder, StatusCode};
use crate::core::*;
use crate::state::AppState;
use crate::util::bear::Bearcap;
pub enum Response {
Reqwest(reqwest::Response),
}
/// Perform an HTTP GET request to the specified URL (supports bearcaps).
/// Use the [`crate::headers`] macro for the `headers` parameter.
///
@ -39,7 +44,7 @@ use crate::util::bear::Bearcap;
/// let response = get(&state, "bear:?t=b4dc0ffee&u=https://www.example.com", None).await?;
/// ```
pub async fn get(
state: &AppState,
_state: &AppState,
url: &str,
headers: Option<&[(GenericHeaderName<'_>, String)]>,
) -> Result<Response> {
@ -52,15 +57,15 @@ pub async fn get(
} else {
client.get(url)
};
perform_request(state, builder, headers.unwrap_or(&[])).await
let response = perform_request(builder, headers.unwrap_or(&[])).await?;
Ok(Response::Reqwest(response))
}
// the state reference will be used for caching
async fn perform_request(
_state: &AppState,
mut builder: RequestBuilder,
headers: &[(GenericHeaderName<'_>, String)],
) -> Result<Response> {
) -> Result<reqwest::Response> {
builder = headers.iter().fold(builder, |b, (k, v)| match k {
GenericHeaderName::Standard(hdr) => b.header(hdr, v),
GenericHeaderName::CustomOwned(hdr) => b.header(hdr, v),
@ -70,6 +75,26 @@ async fn perform_request(
Ok(builder.send().await?)
}
impl Response {
pub fn status(&self) -> StatusCode {
match self {
Response::Reqwest(r) => r.status(),
}
}
pub fn headers(&self) -> &HeaderMap {
match self {
Response::Reqwest(r) => r.headers(),
}
}
pub async fn bytes(self) -> Result<Bytes> {
match self {
Response::Reqwest(r) => r.bytes().await.map_err(Error::from),
}
}
}
/// Generate a list of HTTP request headers for passing to [`get`].
#[macro_export]
macro_rules! headers {
@ -106,3 +131,13 @@ impl<'a> From<HeaderName> for GenericHeaderName<'a> {
GenericHeaderName::Standard(val)
}
}
impl PartialEq<str> for GenericHeaderName<'_> {
fn eq(&self, other: &str) -> bool {
match self {
GenericHeaderName::CustomOwned(s) => s.eq(other),
GenericHeaderName::CustomBorrow(s) => (*s).eq(other),
GenericHeaderName::Standard(v) => v.as_str().eq(other),
}
}
}

View file

@ -1,6 +1,6 @@
pub mod bear;
pub mod crypto;
/// Almost complete implementation of [RFC 8941](https://www.rfc-editor.org/rfc/rfc8941).
/// Almost complete implementation of [RFC 8941](https://www.rfc-editor.org/info/rfc8941).
pub mod header;
/// Wrappers for [`reqwest`].
pub mod http;

View file

@ -25,9 +25,7 @@ impl<'a, T> SliceCursor<'a, T> {
chop: 0,
}
}
}
impl<'a, T> SliceCursor<'a, T> {
/// Return the item at the current position, if any.
pub fn current(&self) -> Option<&'a T> {
self.pos.index().map(|index| &self.data[index])
@ -43,7 +41,7 @@ impl<'a, T> SliceCursor<'a, T> {
self.pos.next_index().map(|index| &self.data[index])
}
/// Return the current item and reverse the cursor.
/// Reverse the cursor to the previous item and return that item.
pub fn prev(&mut self) -> Option<&'a T> {
self.pos.reverse().map(|index| &self.data[index])
}