Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

#### 🐞 Fixes

- Fixed an issue where source URLs that contain a query string would not be parsed.

## 0.18.13

#### 🐞 Fixes
Expand Down
42 changes: 1 addition & 41 deletions crates/schematic/src/config/source.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::cacher::BoxedCacher;
use super::error::ConfigError;
use crate::format::Format;
use crate::helpers::*;
use serde::Deserialize;
use serde::{Serialize, de::DeserializeOwned};
use std::fs;
Expand Down Expand Up @@ -169,44 +170,3 @@ impl Source {
}
}
}

/// Returns true if the value ends in a supported file extension.
pub fn is_source_format(value: &str) -> bool {
value.ends_with(".json")
|| value.ends_with(".pkl")
|| value.ends_with(".toml")
|| value.ends_with(".yaml")
|| value.ends_with(".yml")
}

/// Returns true if the value looks like a file, by checking for `file://`,
/// path separators, or supported file extensions.
pub fn is_file_like(value: &str) -> bool {
value.starts_with("file://")
|| value.starts_with('/')
|| value.starts_with('\\')
|| value.starts_with('.')
|| value.contains('/')
|| value.contains('\\')
|| value.contains('.')
}

/// Returns true if the value looks like a URL, by checking for `http://`, `https://`, or `www`.
pub fn is_url_like(value: &str) -> bool {
value.starts_with("https://") || value.starts_with("http://") || value.starts_with("www")
}

/// Returns true if the value is a secure URL, by checking for `https://`. This check can be
/// bypassed for localhost URLs.
pub fn is_secure_url(value: &str) -> bool {
if value.contains("127.0.0.1") || value.contains("//localhost") {
return true;
}

value.starts_with("https://")
}

/// Strip a leading BOM from the string.
pub fn strip_bom(content: &str) -> &str {
content.trim_start_matches("\u{feff}")
}
12 changes: 7 additions & 5 deletions crates/schematic/src/format.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::helpers::extract_ext;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use thiserror::Error;
Expand Down Expand Up @@ -33,12 +34,13 @@ impl Format {
/// checking for a supported file extension.
pub fn detect(value: &str) -> Result<Format, UnsupportedFormatError> {
let mut available: Vec<&str> = vec![];
let ext = extract_ext(value).unwrap_or_default();

#[cfg(feature = "json")]
{
available.push("JSON");

if value.ends_with(".json") {
if ext == ".json" {
return Ok(Format::Json);
}
}
Expand All @@ -47,7 +49,7 @@ impl Format {
{
available.push("Pkl");

if value.ends_with(".pkl") {
if ext == ".pkl" {
return Ok(Format::Pkl);
}
}
Expand All @@ -56,7 +58,7 @@ impl Format {
{
available.push("RON");

if value.ends_with(".ron") {
if ext == ".ron" {
return Ok(Format::Ron);
}
}
Expand All @@ -65,7 +67,7 @@ impl Format {
{
available.push("TOML");

if value.ends_with(".toml") {
if ext == ".toml" {
return Ok(Format::Toml);
}
}
Expand All @@ -74,7 +76,7 @@ impl Format {
{
available.push("YAML");

if value.ends_with(".yaml") || value.ends_with(".yml") {
if ext == ".yaml" || ext == ".yml" {
return Ok(Format::Yaml);
}
}
Expand Down
57 changes: 57 additions & 0 deletions crates/schematic/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/// Returns true if the value ends in a supported file extension.
pub fn is_source_format(value: &str) -> bool {
extract_ext(value).is_some_and(|ext| {
ext == ".json" || ext == ".pkl" || ext == ".toml" || ext == ".yaml" || ext == ".yml"
})
}

/// Returns true if the value looks like a file, by checking for `file://`,
/// path separators, or supported file extensions.
pub fn is_file_like(value: &str) -> bool {
value.starts_with("file://")
|| value.starts_with('/')
|| value.starts_with('\\')
|| value.starts_with('.')
|| value.contains('/')
|| value.contains('\\')
|| value.contains('.')
}

/// Returns true if the value looks like a URL, by checking for `http://`, `https://`, or `www`.
pub fn is_url_like(value: &str) -> bool {
value.starts_with("https://") || value.starts_with("http://") || value.starts_with("www")
}

/// Returns true if the value is a secure URL, by checking for `https://`. This check can be
/// bypassed for localhost URLs.
pub fn is_secure_url(value: &str) -> bool {
if value.contains("127.0.0.1") || value.contains("//localhost") {
return true;
}

value.starts_with("https://")
}

/// Strip a leading BOM from the string.
pub fn strip_bom(content: &str) -> &str {
content.trim_start_matches("\u{feff}")
}

/// Extract a file extension from the provided file path or URL.
pub fn extract_ext(value: &str) -> Option<&str> {
// Remove any query string
let value = if let Some(index) = value.rfind('?') {
&value[0..index]
} else {
value
};

// And only check the last segment
let value = if let Some(index) = value.rfind('/') {
&value[index + 1..]
} else {
value
};

value.rfind('.').map(|index| &value[index..])
}
1 change: 1 addition & 0 deletions crates/schematic/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(clippy::result_large_err)]

mod format;
pub mod helpers;

#[cfg(feature = "config")]
mod config;
Expand Down
22 changes: 6 additions & 16 deletions crates/schematic/src/validate/extends.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use crate::config::{
ExtendsFrom, Path, PathSegment, ValidateError, ValidateResult, is_file_like, is_secure_url,
is_source_format, is_url_like,
};
use crate::config::{ExtendsFrom, Path, PathSegment, ValidateError, ValidateResult};
use crate::helpers::*;

/// Validate an `extend` value is either a file path or secure URL.
pub fn extends_string<D, C>(
Expand All @@ -19,18 +17,10 @@ pub fn extends_string<D, C>(
));
}

if !value.is_empty() {
let value = if is_url && let Some(index) = value.rfind('?') {
&value[0..index]
} else {
value
};

if !is_source_format(value) {
return Err(ValidateError::new(
"invalid file format, try a supported extension",
));
}
if !value.is_empty() && !is_source_format(value) {
return Err(ValidateError::new(
"invalid file format, try a supported extension",
));
}

if is_url && !is_secure_url(value) {
Expand Down
2 changes: 1 addition & 1 deletion crates/schematic/src/validate/url.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{ValidateError, ValidateResult, map_err};
use crate::config::is_secure_url;
use crate::helpers::is_secure_url;
pub use garde::rules::url::Url;

/// Validate a string matches a URL.
Expand Down
39 changes: 39 additions & 0 deletions crates/schematic/tests/helpers_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use schematic::helpers::extract_ext;

mod ext {
use super::*;

#[test]
fn works_on_files() {
assert!(extract_ext("file").is_none());
assert_eq!(extract_ext("file.json").unwrap(), ".json");
assert_eq!(extract_ext("dir/file.yaml").unwrap(), ".yaml");
assert_eq!(extract_ext("../file.toml").unwrap(), ".toml");
assert_eq!(extract_ext("/root/file.other.json").unwrap(), ".json");
}

#[test]
fn works_on_urls() {
assert!(extract_ext("https://domain.com/file").is_none());
assert_eq!(
extract_ext("https://domain.com/file.json").unwrap(),
".json"
);
assert_eq!(
extract_ext("http://domain.com/dir/file.yaml").unwrap(),
".yaml"
);
assert_eq!(
extract_ext("https://domain.com/file.toml?query").unwrap(),
".toml"
);
assert_eq!(
extract_ext("http://domain.com/root/file.other.json").unwrap(),
".json"
);
assert_eq!(
extract_ext("https://domain.com/other.segment/file.toml?query").unwrap(),
".toml"
);
}
}