use crate::verification::types::ValidationError;
#[cfg(feature = "url")]
use anodized::spec;
#[cfg(feature = "url")]
use url::Url;
#[cfg(not(kani))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UrlValid(Url);
#[cfg(kani)]
#[derive(Debug, Clone)]
pub struct UrlValid(std::marker::PhantomData<Url>);
#[cfg(not(kani))]
impl UrlValid {
#[spec(requires: [!value.is_empty()])]
pub fn new(value: &str) -> Result<Self, ValidationError> {
Url::parse(value)
.map(Self)
.map_err(|_| ValidationError::UrlInvalid)
}
pub fn from_url(url: Url) -> Self {
Self(url)
}
pub fn get(&self) -> &Url {
&self.0
}
pub fn into_inner(self) -> Url {
self.0
}
}
#[cfg(kani)]
impl UrlValid {
pub fn new(_value: &str) -> Result<Self, ValidationError> {
let is_valid: bool = kani::any();
if is_valid {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlInvalid)
}
}
pub fn from_url(_url: Url) -> Self {
Self(std::marker::PhantomData)
}
pub fn get(&self) -> &Url {
panic!("UrlValid::get() not available in Kani mode - use symbolic validation")
}
pub fn into_inner(self) -> Url {
panic!("UrlValid::into_inner() not available in Kani mode - use symbolic validation")
}
}
#[cfg(not(kani))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UrlHttps(Url);
#[cfg(kani)]
#[derive(Debug, Clone)]
pub struct UrlHttps(std::marker::PhantomData<Url>);
#[cfg(not(kani))]
impl UrlHttps {
#[spec(requires: [!value.is_empty()])]
pub fn new(value: &str) -> Result<Self, ValidationError> {
let url = Url::parse(value).map_err(|_| ValidationError::UrlInvalid)?;
if url.scheme() == "https" {
Ok(Self(url))
} else {
Err(ValidationError::UrlNotHttps)
}
}
pub fn from_url(url: Url) -> Result<Self, ValidationError> {
if url.scheme() == "https" {
Ok(Self(url))
} else {
Err(ValidationError::UrlNotHttps)
}
}
pub fn get(&self) -> &Url {
&self.0
}
pub fn into_inner(self) -> Url {
self.0
}
}
#[cfg(kani)]
impl UrlHttps {
pub fn new(_value: &str) -> Result<Self, ValidationError> {
let is_valid: bool = kani::any();
let is_https: bool = kani::any();
if !is_valid {
Err(ValidationError::UrlInvalid)
} else if is_https {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlNotHttps)
}
}
pub fn from_url(_url: Url) -> Result<Self, ValidationError> {
let is_https: bool = kani::any();
if is_https {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlNotHttps)
}
}
pub fn get(&self) -> &Url {
panic!("UrlHttps::get() not available in Kani mode - use symbolic validation")
}
pub fn into_inner(self) -> Url {
panic!("UrlHttps::into_inner() not available in Kani mode - use symbolic validation")
}
}
#[cfg(not(kani))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UrlHttp(Url);
#[cfg(kani)]
#[derive(Debug, Clone)]
pub struct UrlHttp(std::marker::PhantomData<Url>);
#[cfg(not(kani))]
impl UrlHttp {
#[spec(requires: [!value.is_empty()])]
pub fn new(value: &str) -> Result<Self, ValidationError> {
let url = Url::parse(value).map_err(|_| ValidationError::UrlInvalid)?;
if url.scheme() == "http" {
Ok(Self(url))
} else {
Err(ValidationError::UrlNotHttp)
}
}
pub fn from_url(url: Url) -> Result<Self, ValidationError> {
if url.scheme() == "http" {
Ok(Self(url))
} else {
Err(ValidationError::UrlNotHttp)
}
}
pub fn get(&self) -> &Url {
&self.0
}
pub fn into_inner(self) -> Url {
self.0
}
}
#[cfg(kani)]
impl UrlHttp {
pub fn new(_value: &str) -> Result<Self, ValidationError> {
let is_valid: bool = kani::any();
let is_http: bool = kani::any();
if !is_valid {
Err(ValidationError::UrlInvalid)
} else if is_http {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlNotHttp)
}
}
pub fn from_url(_url: Url) -> Result<Self, ValidationError> {
let is_http: bool = kani::any();
if is_http {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlNotHttp)
}
}
pub fn get(&self) -> &Url {
panic!("UrlHttp::get() not available in Kani mode - use symbolic validation")
}
pub fn into_inner(self) -> Url {
panic!("UrlHttp::into_inner() not available in Kani mode - use symbolic validation")
}
}
#[cfg(not(kani))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UrlWithHost(Url);
#[cfg(kani)]
#[derive(Debug, Clone)]
pub struct UrlWithHost(std::marker::PhantomData<Url>);
#[cfg(not(kani))]
impl UrlWithHost {
#[spec(requires: [!value.is_empty()])]
pub fn new(value: &str) -> Result<Self, ValidationError> {
let url = Url::parse(value).map_err(|_| ValidationError::UrlInvalid)?;
if url.host().is_some() {
Ok(Self(url))
} else {
Err(ValidationError::UrlNoHost)
}
}
pub fn from_url(url: Url) -> Result<Self, ValidationError> {
if url.host().is_some() {
Ok(Self(url))
} else {
Err(ValidationError::UrlNoHost)
}
}
pub fn get(&self) -> &Url {
&self.0
}
pub fn into_inner(self) -> Url {
self.0
}
}
#[cfg(kani)]
impl UrlWithHost {
pub fn new(_value: &str) -> Result<Self, ValidationError> {
let is_valid: bool = kani::any();
let has_host: bool = kani::any();
if !is_valid {
Err(ValidationError::UrlInvalid)
} else if has_host {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlNoHost)
}
}
pub fn from_url(_url: Url) -> Result<Self, ValidationError> {
let has_host: bool = kani::any();
if has_host {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlNoHost)
}
}
pub fn get(&self) -> &Url {
panic!("UrlWithHost::get() not available in Kani mode - use symbolic validation")
}
pub fn into_inner(self) -> Url {
panic!("UrlWithHost::into_inner() not available in Kani mode - use symbolic validation")
}
}
#[cfg(not(kani))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UrlCanBeBase(Url);
#[cfg(kani)]
#[derive(Debug, Clone)]
pub struct UrlCanBeBase(std::marker::PhantomData<Url>);
#[cfg(not(kani))]
impl UrlCanBeBase {
#[spec(requires: [!value.is_empty()])]
pub fn new(value: &str) -> Result<Self, ValidationError> {
let url = Url::parse(value).map_err(|_| ValidationError::UrlInvalid)?;
if url.cannot_be_a_base() {
Err(ValidationError::UrlCannotBeBase)
} else {
Ok(Self(url))
}
}
pub fn from_url(url: Url) -> Result<Self, ValidationError> {
if url.cannot_be_a_base() {
Err(ValidationError::UrlCannotBeBase)
} else {
Ok(Self(url))
}
}
pub fn get(&self) -> &Url {
&self.0
}
pub fn into_inner(self) -> Url {
self.0
}
}
#[cfg(kani)]
impl UrlCanBeBase {
pub fn new(_value: &str) -> Result<Self, ValidationError> {
let is_valid: bool = kani::any();
let can_be_base: bool = kani::any();
if !is_valid {
Err(ValidationError::UrlInvalid)
} else if can_be_base {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlCannotBeBase)
}
}
pub fn from_url(_url: Url) -> Result<Self, ValidationError> {
let can_be_base: bool = kani::any();
if can_be_base {
Ok(Self(std::marker::PhantomData))
} else {
Err(ValidationError::UrlCannotBeBase)
}
}
pub fn get(&self) -> &Url {
panic!("UrlCanBeBase::get() not available in Kani mode - use symbolic validation")
}
pub fn into_inner(self) -> Url {
panic!("UrlCanBeBase::into_inner() not available in Kani mode - use symbolic validation")
}
}
use crate::{ElicitCommunicator, ElicitResult, Elicitation, Prompt};
pub use crate::primitives::url::UrlStyle;
impl Prompt for UrlValid {
fn prompt() -> Option<&'static str> {
Some("Enter a valid URL:")
}
}
impl Elicitation for UrlValid {
type Style = UrlStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let prompt = communicator
.style_context()
.prompt_for_type::<url::Url>("value", "URL", &crate::style::PromptContext::new(0, 1))
.unwrap_or(None)
.unwrap_or_else(|| Self::prompt().unwrap().to_string());
tracing::debug!(%prompt, "Eliciting UrlValid with server-side send_prompt");
let response = communicator.send_prompt(&prompt).await?;
let url = url::Url::parse(response.trim()).map_err(|e| {
crate::ElicitError::new(crate::ElicitErrorKind::ParseError(format!(
"Invalid URL: {}",
e
)))
})?;
Ok(Self::from_url(url))
}
fn kani_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::kani_url_valid()
}
fn verus_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::verus_type_stub("UrlValid")
}
fn creusot_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::creusot_type_stub("UrlValid")
}
}
impl Prompt for UrlHttps {
fn prompt() -> Option<&'static str> {
Some("Enter an HTTPS URL:")
}
}
impl Elicitation for UrlHttps {
type Style = UrlStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let value = url::Url::elicit(communicator).await?;
Self::from_url(value).map_err(crate::ElicitError::from)
}
fn kani_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::kani_url_https()
}
fn verus_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::verus_type_stub("UrlHttps")
}
fn creusot_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::creusot_type_stub("UrlHttps")
}
}
impl Prompt for UrlHttp {
fn prompt() -> Option<&'static str> {
Some("Enter an HTTP or HTTPS URL:")
}
}
impl Elicitation for UrlHttp {
type Style = UrlStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let value = url::Url::elicit(communicator).await?;
Self::from_url(value).map_err(crate::ElicitError::from)
}
fn kani_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::kani_url_https()
}
fn verus_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::verus_type_stub("UrlHttp")
}
fn creusot_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::creusot_type_stub("UrlHttp")
}
}
impl Prompt for UrlWithHost {
fn prompt() -> Option<&'static str> {
Some("Enter a URL with a host:")
}
}
impl Elicitation for UrlWithHost {
type Style = UrlStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let value = url::Url::elicit(communicator).await?;
Self::from_url(value).map_err(crate::ElicitError::from)
}
fn kani_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::kani_url_with_host()
}
fn verus_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::verus_type_stub("UrlWithHost")
}
fn creusot_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::creusot_type_stub("UrlWithHost")
}
}
impl Prompt for UrlCanBeBase {
fn prompt() -> Option<&'static str> {
Some("Enter a base URL:")
}
}
impl Elicitation for UrlCanBeBase {
type Style = UrlStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let value = url::Url::elicit(communicator).await?;
Self::from_url(value).map_err(crate::ElicitError::from)
}
fn kani_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::kani_url_can_be_base()
}
fn verus_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::verus_type_stub("UrlCanBeBase")
}
fn creusot_proof() -> proc_macro2::TokenStream {
crate::verification::proof_helpers::creusot_type_stub("UrlCanBeBase")
}
}
#[cfg(feature = "url")]
mod serde_bridge {
use super::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
macro_rules! impl_url_serde_bridge {
($ty:ident, $scheme_desc:expr) => {
impl Serialize for $ty {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
#[cfg(not(kani))]
return self.get().as_str().serialize(s);
#[cfg(kani)]
return "".serialize(s);
}
}
impl<'de> Deserialize<'de> for $ty {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::new(&s).map_err(serde::de::Error::custom)
}
}
impl JsonSchema for $ty {
fn schema_name() -> ::std::borrow::Cow<'static, str> {
stringify!($ty).into()
}
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"format": "uri",
"description": $scheme_desc
})
}
}
};
}
impl_url_serde_bridge!(UrlValid, "A valid URL (any scheme).");
impl_url_serde_bridge!(UrlHttps, "An HTTPS URL. Must use the https:// scheme.");
impl_url_serde_bridge!(UrlHttp, "An HTTP URL. Must use the http:// scheme.");
impl_url_serde_bridge!(UrlWithHost, "A URL that must include a hostname.");
impl_url_serde_bridge!(UrlCanBeBase, "A URL that can be used as a base URL.");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url_valid() {
assert!(UrlValid::new("https://example.com").is_ok());
assert!(UrlValid::new("http://localhost:8080/path").is_ok());
assert!(UrlValid::new("ftp://files.example.com").is_ok());
assert!(UrlValid::new("not a url").is_err());
assert!(UrlValid::new("").is_err());
}
#[test]
fn test_url_https() {
assert!(UrlHttps::new("https://example.com").is_ok());
assert!(UrlHttps::new("https://secure.example.com/path?query=1").is_ok());
assert!(UrlHttps::new("http://example.com").is_err());
assert!(UrlHttps::new("ftp://example.com").is_err());
assert!(UrlHttps::new("not a url").is_err());
}
#[test]
fn test_url_http() {
assert!(UrlHttp::new("http://example.com").is_ok());
assert!(UrlHttp::new("http://localhost:8080/api").is_ok());
assert!(UrlHttp::new("https://example.com").is_err());
assert!(UrlHttp::new("ftp://example.com").is_err());
assert!(UrlHttp::new("not a url").is_err());
let url = Url::parse("http://example.com").unwrap();
let http_url = UrlHttp::from_url(url).unwrap();
assert_eq!(http_url.get().scheme(), "http");
let inner = http_url.into_inner();
assert_eq!(inner.as_str(), "http://example.com/");
}
#[test]
fn test_url_with_host() {
assert!(UrlWithHost::new("https://example.com").is_ok());
assert!(UrlWithHost::new("http://192.168.1.1:8080").is_ok());
assert!(UrlWithHost::new("mailto:user@example.com").is_err()); assert!(UrlWithHost::new("data:text/plain,hello").is_err()); }
#[test]
fn test_url_can_be_base() {
assert!(UrlCanBeBase::new("https://example.com").is_ok());
assert!(UrlCanBeBase::new("http://example.com/path/").is_ok());
assert!(UrlCanBeBase::new("mailto:user@example.com").is_err()); assert!(UrlCanBeBase::new("data:text/plain,hello").is_err()); }
#[test]
fn test_url_accessors() {
let url = UrlValid::new("https://example.com/path").unwrap();
assert_eq!(url.get().scheme(), "https");
assert_eq!(url.get().host_str(), Some("example.com"));
let inner = url.into_inner();
assert_eq!(inner.as_str(), "https://example.com/path");
}
}
#[cfg(all(feature = "url", not(kani)))]
mod emit_impls {
use super::*;
use crate::emit_code::ToCodeLiteral;
use proc_macro2::TokenStream;
macro_rules! impl_to_code_literal_url {
($T:ident) => {
impl ToCodeLiteral for $T {
fn to_code_literal(&self) -> TokenStream {
let s = self.get().as_str();
let msg = concat!("valid ", stringify!($T));
let type_path: TokenStream = concat!("elicitation::", stringify!($T))
.parse()
.expect("valid type path");
quote::quote! { #type_path::new(#s).expect(#msg) }
}
}
};
}
impl_to_code_literal_url!(UrlValid);
impl_to_code_literal_url!(UrlHttps);
impl_to_code_literal_url!(UrlHttp);
impl_to_code_literal_url!(UrlWithHost);
impl_to_code_literal_url!(UrlCanBeBase);
}