fix(rust): enforce lint on all files + fix lint issues (#1394)

This commit is contained in:
Prayag Bhakar
2026-08-21 01:38:45 -04:00
committed by GitHub
parent 071d4df034
commit 6fb9a35336
40 changed files with 226 additions and 242 deletions
+13
View File
@@ -10,6 +10,19 @@ repository = "https://github.com/sqlpage/SQLPage"
documentation = "https://docs.rs/sqlpage"
include = ["/src", "/README.md", "/build.rs", "/sqlpage"]
[lints.rust]
elided_lifetimes_in_paths = "deny"
unreachable_pub = "deny"
unused_qualifications = "deny"
[lints.clippy]
pedantic = { level = "deny", priority = -1 }
missing_errors_doc = "allow"
missing_panics_doc = "allow"
dbg_macro = "deny"
todo = "deny"
unimplemented = "deny"
[profile.superoptimized]
inherits = "release"
strip = "debuginfo"
+19 -19
View File
@@ -84,11 +84,7 @@ async fn process_input_file(client: &awc::Client, path_out: &Path, original: Fil
.expect("Unable to write compressed frontend asset");
}
async fn copy_url_to_opened_file(
client: &awc::Client,
url: &str,
outfile: &mut impl std::io::Write,
) {
async fn copy_url_to_opened_file(client: &awc::Client, url: &str, outfile: &mut impl Write) {
// If the file has been downloaded manually, use it
let cached_file_path = make_url_path(url);
if !cached_file_path.exists() {
@@ -98,9 +94,9 @@ async fn copy_url_to_opened_file(
copy_cached_to_opened_file(&cached_file_path, outfile);
}
fn copy_cached_to_opened_file(source: &Path, outfile: &mut impl std::io::Write) {
let reader = std::fs::File::open(source).unwrap();
let mut buf = std::io::BufReader::new(reader);
fn copy_cached_to_opened_file(source: &Path, outfile: &mut impl Write) {
let reader = File::open(source).unwrap();
let mut buf = BufReader::new(reader);
// Not async, but performance should not really matter here
std::io::copy(&mut buf, outfile).unwrap();
}
@@ -112,9 +108,12 @@ async fn download_url_to_path(client: &awc::Client, url: &str, path: &Path) {
loop {
match client.get(url).send().await {
Ok(mut resp) => {
if resp.status() != 200 {
panic!("Received {} status code from {}", resp.status(), url);
}
assert!(
resp.status() == 200,
"Received {} status code from {}",
resp.status(),
url
);
let bytes = resp.body().limit(128 * 1024 * 1024).await.unwrap();
std::fs::write(path, &bytes)
.expect("Failed to write external frontend dependency to local file");
@@ -122,7 +121,7 @@ async fn download_url_to_path(client: &awc::Client, url: &str, path: &Path) {
}
Err(err) => {
if attempt >= max_attempts {
let path = make_url_path(url);
let path = make_url_path(url).display().to_string();
panic!(
"We need to download external frontend dependencies to build the static frontend. \n\
Could not download static asset after {max_attempts} attempts. You can manually download the file with: \n\
@@ -187,15 +186,16 @@ async fn download_tabler_icons(client: Rc<awc::Client>, sprite_url: &str) {
file.write_all(b"]").unwrap();
}
fn take_between<'a>(s: &mut &'a str, start: &str, end: &str) -> Option<&'a str> {
let start_index = s.find(start)?;
let end_index = s[start_index + start.len()..].find(end)?;
let result = &s[start_index + start.len()..][..end_index];
*s = &s[start_index + start.len() + end_index + end.len()..];
Some(result)
}
fn extract_icons_from_sprite(sprite_content: &[u8], mut callback: impl FnMut(&str, &str)) {
let mut sprite_str = std::str::from_utf8(sprite_content).unwrap();
fn take_between<'a>(s: &mut &'a str, start: &str, end: &str) -> Option<&'a str> {
let start_index = s.find(start)?;
let end_index = s[start_index + start.len()..].find(end)?;
let result = &s[start_index + start.len()..][..end_index];
*s = &s[start_index + start.len() + end_index + end.len()..];
Some(result)
}
while let Some(mut symbol_tag) = take_between(&mut sprite_str, "<symbol", "</symbol>") {
let id = take_between(&mut symbol_tag, "id=\"tabler-", "\"").expect("id not found");
let content_start = symbol_tag.find('>').unwrap() + 1;
+3 -3
View File
@@ -460,7 +460,7 @@ fn configuration_directory() -> PathBuf {
PathBuf::from("./sqlpage")
}
fn cannonicalize_if_possible(path: &std::path::Path) -> PathBuf {
fn cannonicalize_if_possible(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_owned())
}
@@ -674,7 +674,7 @@ fn create_default_database(configuration_directory: &Path) -> String {
#[cfg(any(test, not(feature = "lambda-web")))]
fn encode_uri(path: &Path) -> std::borrow::Cow<'_, str> {
const ASCII_SET: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
const ASCII_SET: &AsciiSet = &percent_encoding::NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
@@ -1000,7 +1000,7 @@ mod test {
let _lock = ENV_LOCK
.lock()
.expect("Another test panicked while holding the lock");
let temp_dir = std::env::temp_dir().join("sqlpage_test");
let temp_dir = env::temp_dir().join("sqlpage_test");
std::fs::create_dir_all(&temp_dir).unwrap();
let config_file_path = temp_dir.join("sqlpage.json");
let config_web_dir = temp_dir.join("config/web");
+4 -4
View File
@@ -49,7 +49,7 @@ pub(crate) struct FileSystem {
}
impl FileSystem {
pub async fn init(local_root: impl Into<PathBuf>, db: &Database) -> Self {
pub(crate) async fn init(local_root: impl Into<PathBuf>, db: &Database) -> Self {
Self {
local_root: local_root.into(),
db_fs_queries: match DbFsQueries::init(db).await {
@@ -68,7 +68,7 @@ impl FileSystem {
}
}
pub async fn modified_since(
pub(crate) async fn modified_since(
&self,
app_state: &AppState,
access: FileAccess<'_>,
@@ -99,7 +99,7 @@ impl FileSystem {
}
}
pub async fn read_to_string(
pub(crate) async fn read_to_string(
&self,
app_state: &AppState,
access: FileAccess<'_>,
@@ -127,7 +127,7 @@ impl FileSystem {
})
}
pub async fn read_file(
pub(crate) async fn read_file(
&self,
app_state: &AppState,
access: FileAccess<'_>,
+1 -4
View File
@@ -1,6 +1,3 @@
#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
//! [SQLPage](https://sql-page.com) is a high-performance web server that converts SQL queries
//! into dynamic web applications by rendering [handlebars templates](https://sql-page.com/custom_components.sql)
//! with data coming from SQL queries declared in `.sql` files.
@@ -135,7 +132,7 @@ impl AppState {
),
);
let oidc_state = crate::webserver::oidc::initialize_oidc_state(config).await?;
let oidc_state = webserver::oidc::initialize_oidc_state(config).await?;
let telemetry_metrics =
TelemetryMetrics::new(&db.connection, db.info.database_type.otel_name());
+1 -1
View File
@@ -60,7 +60,7 @@ fn init_logging() -> anyhow::Result<()> {
let otel_active = telemetry::init_telemetry()?;
match load_env {
Ok(path) => log::info!("Loaded environment variables from {path:?}"),
Ok(path) => log::info!("Loaded environment variables from {}", path.display()),
Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => log::debug!(
"No .env file found, using only environment variables and configuration files"
),
+12 -20
View File
@@ -367,7 +367,7 @@ impl HeaderContext {
}
fn log(self, data: &JsonValue) -> anyhow::Result<PageContext> {
handle_log_component(&self.request_context.source_path, Option::None, data)?;
handle_log_component(&self.request_context.source_path, None, data)?;
Ok(PageContext::Header(self))
}
@@ -555,7 +555,7 @@ impl AnyRenderBodyContext {
}
}
pub struct JsonBodyRenderer<W: std::io::Write> {
pub struct JsonBodyRenderer<W: Write> {
writer: W,
is_first: bool,
prefix: &'static [u8],
@@ -563,7 +563,7 @@ pub struct JsonBodyRenderer<W: std::io::Write> {
separator: &'static [u8],
}
impl<W: std::io::Write> JsonBodyRenderer<W> {
impl<W: Write> JsonBodyRenderer<W> {
pub fn new_array(writer: W) -> JsonBodyRenderer<W> {
let mut renderer = Self {
writer,
@@ -741,7 +741,7 @@ impl CsvBodyRenderer {
}
#[allow(clippy::module_name_repetitions)]
pub struct HtmlRenderContext<W: std::io::Write> {
pub struct HtmlRenderContext<W: Write> {
app_state: Arc<AppState>,
pub writer: W,
current_component: Option<SplitTemplateRenderer>,
@@ -754,7 +754,7 @@ const DEFAULT_COMPONENT: &str = "table";
const PAGE_SHELL_COMPONENT: &str = "shell";
const FRAGMENT_SHELL_COMPONENT: &str = "shell-empty";
impl<W: std::io::Write> HtmlRenderContext<W> {
impl<W: Write> HtmlRenderContext<W> {
pub async fn new(
app_state: Arc<AppState>,
request_context: RequestContext,
@@ -1023,11 +1023,11 @@ fn handle_log_component(
Ok(())
}
struct HandlebarWriterOutput<W: std::io::Write>(W);
struct HandlebarWriterOutput<W: Write>(W);
impl<W: std::io::Write> handlebars::Output for HandlebarWriterOutput<W> {
impl<W: Write> handlebars::Output for HandlebarWriterOutput<W> {
fn write(&mut self, seg: &str) -> std::io::Result<()> {
std::io::Write::write_all(&mut self.0, seg.as_bytes())
Write::write_all(&mut self.0, seg.as_bytes())
}
}
@@ -1043,7 +1043,7 @@ pub struct SplitTemplateRenderer {
}
const _: () = assert!(
std::mem::size_of::<SplitTemplateRenderer>() <= 64,
size_of::<SplitTemplateRenderer>() <= 64,
"SplitTemplateRenderer should be small enough to be allocated on the stack"
);
@@ -1072,11 +1072,7 @@ impl SplitTemplateRenderer {
.unwrap_or_default()
}
fn render_start<W: std::io::Write>(
&mut self,
writer: W,
data: JsonValue,
) -> Result<(), RenderError> {
fn render_start<W: Write>(&mut self, writer: W, data: JsonValue) -> Result<(), RenderError> {
log::trace!(
"Starting rendering of a template{} with the following top-level parameters: {data}",
self.split_template
@@ -1108,11 +1104,7 @@ impl SplitTemplateRenderer {
Ok(())
}
fn render_item<W: std::io::Write>(
&mut self,
writer: W,
data: JsonValue,
) -> Result<(), RenderError> {
fn render_item<W: Write>(&mut self, writer: W, data: JsonValue) -> Result<(), RenderError> {
log::trace!("Rendering a new item in the page: {data:?}");
if let Some(local_vars) = self.local_vars.take() {
let mut render_context = handlebars::RenderContext::new(None);
@@ -1144,7 +1136,7 @@ impl SplitTemplateRenderer {
Ok(())
}
fn render_end<W: std::io::Write>(&mut self, writer: W) -> Result<(), RenderError> {
fn render_end<W: Write>(&mut self, writer: W) -> Result<(), RenderError> {
log::trace!(
"Closing a template {}",
self.split_template
+2 -2
View File
@@ -450,7 +450,7 @@ mod logfmt {
}
impl LogfmtLayer {
pub fn new() -> Self {
pub(super) fn new() -> Self {
Self {
stdout_colors: io::stdout().is_terminal(),
stderr_colors: io::stderr().is_terminal(),
@@ -458,7 +458,7 @@ mod logfmt {
}
}
pub fn test_writer() -> Self {
pub(super) fn test_writer() -> Self {
Self {
stdout_colors: false,
stderr_colors: false,
+14 -14
View File
@@ -168,7 +168,7 @@ fn to_array_helper(v: &JsonValue) -> JsonValue {
struct StaticPathHelper(String);
impl CanHelp for StaticPathHelper {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
let static_file = match args {
[v] => v.value(),
_ => return Err("expected one argument".to_string()),
@@ -192,7 +192,7 @@ impl CanHelp for StaticPathHelper {
struct AppConfigHelper(AppConfig);
impl CanHelp for AppConfigHelper {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
let static_file = match args {
[v] => v.value(),
_ => return Err("expected one argument".to_string()),
@@ -223,7 +223,7 @@ impl HelperDef for IconImgHelper {
_rc: &mut handlebars::RenderContext<'reg, 'rc>,
writer: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let null = handlebars::JsonValue::Null;
let null = JsonValue::Null;
let [name, size] = [0, 1].map(|i| helper.params().get(i).map_or(&null, PathAndJson::value));
let size = size.as_u64().unwrap_or(24);
let content = name.as_str().and_then(|name| ICON_MAP.get(name));
@@ -315,7 +315,7 @@ impl MarkdownHelper {
}
impl CanHelp for MarkdownHelper {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
let (markdown_src_value, preset_name) = match args {
[v] => (v.value(), "default"),
[v, preset] => {
@@ -502,7 +502,7 @@ fn loose_eq_helper(a: &JsonValue, b: &JsonValue) -> JsonValue {
pub struct HelperCheckTruthy(bool);
impl CanHelp for HelperCheckTruthy {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
for arg in args {
if arg.value().is_truthy(false) == self.0 {
return Ok(arg.value().clone());
@@ -517,11 +517,11 @@ impl CanHelp for HelperCheckTruthy {
}
trait CanHelp: Send + Sync + 'static {
fn call(&self, v: &[PathAndJson]) -> Result<JsonValue, String>;
fn call(&self, v: &[PathAndJson<'_>]) -> Result<JsonValue, String>;
}
impl CanHelp for H0 {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
match args {
[] => Ok(self()),
_ => Err("expected no arguments".to_string()),
@@ -530,7 +530,7 @@ impl CanHelp for H0 {
}
impl CanHelp for H {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
match args {
[v] => Ok(self(v.value())),
_ => Err("expected one argument".to_string()),
@@ -539,7 +539,7 @@ impl CanHelp for H {
}
impl CanHelp for EH {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
match args {
[v] => self(v.value()).map_err(|e| e.to_string()),
_ => Err("expected one argument".to_string()),
@@ -548,7 +548,7 @@ impl CanHelp for EH {
}
impl CanHelp for HH {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
match args {
[a, b] => Ok(self(a.value(), b.value())),
_ => Err("expected two arguments".to_string()),
@@ -557,7 +557,7 @@ impl CanHelp for HH {
}
impl CanHelp for HHH {
fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
fn call(&self, args: &[PathAndJson<'_>]) -> Result<JsonValue, String> {
match args {
[a, b, c] => Ok(self(a.value(), b.value(), c.value())),
_ => Err("expected three arguments".to_string()),
@@ -569,14 +569,14 @@ struct JFun<F: CanHelp> {
name: &'static str,
fun: F,
}
impl<F: CanHelp> handlebars::HelperDef for JFun<F> {
impl<F: CanHelp> HelperDef for JFun<F> {
fn call_inner<'reg: 'rc, 'rc>(
&self,
helper: &handlebars::Helper<'rc>,
_r: &'reg Handlebars<'reg>,
_: &'rc Context,
_rc: &mut handlebars::RenderContext<'reg, 'rc>,
) -> Result<handlebars::ScopedJson<'rc>, RenderError> {
) -> Result<ScopedJson<'rc>, RenderError> {
let result = self
.fun
.call(helper.params().as_slice())
@@ -585,7 +585,7 @@ impl<F: CanHelp> handlebars::HelperDef for JFun<F> {
}
}
fn register_helper(h: &mut Handlebars, name: &'static str, fun: impl CanHelp) {
fn register_helper(h: &mut Handlebars<'_>, name: &'static str, fun: impl CanHelp) {
h.register_helper(name, Box::new(JFun { name, fun }));
}
+1 -1
View File
@@ -78,7 +78,7 @@ pub struct AllTemplates {
split_templates: FileCache<SplitTemplate>,
}
const STATIC_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/sqlpage/templates");
const STATIC_TEMPLATES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/sqlpage/templates");
impl AllTemplates {
pub fn init(config: &AppConfig) -> anyhow::Result<Self> {
+4 -7
View File
@@ -27,10 +27,7 @@ impl Database {
set_database_password(&mut connect_options, password);
}
connect_options.log_statements(log::LevelFilter::Trace);
connect_options.log_slow_statements(
log::LevelFilter::Warn,
std::time::Duration::from_millis(250),
);
connect_options.log_slow_statements(log::LevelFilter::Warn, Duration::from_millis(250));
log::debug!(
"Connecting to a {:?} database on {}",
connect_options.kind(),
@@ -147,7 +144,7 @@ fn add_on_return_to_pool(config: &AppConfig, pool_options: PoolOptions<Any>) ->
}
fn on_return_to_pool(
conn: &mut sqlx::any::AnyConnection,
conn: &mut AnyConnection,
meta: sqlx::pool::PoolConnectionMetadata,
sql: std::sync::Arc<String>,
) -> BoxFuture<'_, Result<bool, sqlx::error::Error>> {
@@ -229,9 +226,9 @@ fn set_custom_connect_options_sqlite(
) {
for extension_name in &config.sqlite_extensions {
log::info!("Loading SQLite extension: {extension_name}");
*sqlite_options = std::mem::take(sqlite_options).extension(extension_name.clone());
*sqlite_options = take(sqlite_options).extension(extension_name.clone());
}
*sqlite_options = std::mem::take(sqlite_options)
*sqlite_options = take(sqlite_options)
.collation("NOCASE", |a, b| a.to_lowercase().cmp(&b.to_lowercase()))
.function(make_sqlite_fun("upper", str::to_uppercase))
.function(make_sqlite_fun("lower", str::to_lowercase));
+4 -4
View File
@@ -34,9 +34,9 @@ pub(super) struct CsvImport {
}
enum CopyCsvOption<'a> {
Legacy(&'a sqlparser::ast::CopyLegacyOption),
CopyLegacyCsvOption(&'a sqlparser::ast::CopyLegacyCsvOption),
New(&'a sqlparser::ast::CopyOption),
Legacy(&'a CopyLegacyOption),
CopyLegacyCsvOption(&'a CopyLegacyCsvOption),
New(&'a CopyOption),
}
impl CopyCsvOption<'_> {
@@ -105,7 +105,7 @@ pub(super) fn extract_csv_copy_statement(stmt: &mut Statement) -> Option<CsvImpo
}
};
let all_options: Vec<CopyCsvOption> = legacy_options
let all_options: Vec<CopyCsvOption<'_>> = legacy_options
.iter()
.flat_map(|o| match o {
CopyLegacyOption::Csv(o) => {
+5 -5
View File
@@ -97,7 +97,7 @@ impl std::error::Error for NicePositionedError {
/// Display a database error without any position information
#[must_use]
pub fn display_db_error(
pub(super) fn display_db_error(
source_file: &Path,
query: &str,
db_err: sqlx::error::Error,
@@ -112,7 +112,7 @@ pub fn display_db_error(
/// Display a database error with a highlighted line and character offset.
#[must_use]
pub fn display_stmt_db_error(
pub(super) fn display_stmt_db_error(
source_file: &Path,
query: &str,
query_position: SourceSpan,
@@ -127,7 +127,7 @@ pub fn display_stmt_db_error(
}
#[must_use]
pub fn display_stmt_error(
pub(super) fn display_stmt_error(
source_file: &Path,
query_position: SourceSpan,
error: anyhow::Error,
@@ -140,14 +140,14 @@ pub fn display_stmt_error(
}
/// Highlight a line with a character offset.
pub fn highlight_line_offset<W: std::fmt::Write>(msg: &mut W, line: &str, offset: usize) {
pub(super) fn highlight_line_offset<W: Write>(msg: &mut W, line: &str, offset: usize) {
writeln!(msg, "{line}").unwrap();
writeln!(msg, "{}⬆️", " ".repeat(offset)).unwrap();
}
/// Highlight an error given a line and a character offset
/// line and `col_num` are 1-based
pub fn quote_source_with_highlight(source: &str, line_num: u64, col_num: u64) -> String {
pub(super) fn quote_source_with_highlight(source: &str, line_num: u64, col_num: u64) -> String {
let mut msg = String::new();
let col_num_usize = usize::try_from(col_num)
.unwrap_or_default()
+6 -6
View File
@@ -298,7 +298,7 @@ pub fn stream_query_results_with_conn<'a>(
fn with_stmt_position(
source_file: &Path,
query_position: super::sql::SourceSpan,
query_position: SourceSpan,
error: anyhow::Error,
) -> anyhow::Error {
if error.downcast_ref::<ErrorWithStatus>().is_some() {
@@ -335,7 +335,7 @@ async fn execute_single_row(
query: &SingleRowQuery,
req: &ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<serde_json::Value> {
) -> anyhow::Result<Value> {
let mut map = serde_json::Map::with_capacity(query.columns.len());
let mut inputs = NoInputs;
for column in &query.columns {
@@ -346,7 +346,7 @@ async fn execute_single_row(
.into_json();
map = add_value_to_map(map, (column.name.clone(), value));
}
Ok(serde_json::Value::Object(map))
Ok(Value::Object(map))
}
async fn try_rollback_transaction(db_connection: &mut AnyConnection) {
@@ -732,7 +732,7 @@ async fn evaluate_computed_columns(
result: &mut QueryResult,
db_connection: &mut DbConn,
) -> anyhow::Result<()> {
if let DbItem::Row(serde_json::Value::Object(results)) = &mut result.item {
if let DbItem::Row(Value::Object(results)) = &mut result.item {
for column in columns {
let value = column
.value
@@ -1011,7 +1011,7 @@ mod tests {
sqlpage.exception.details = tracing::field::Empty,
db.response.returned_rows = tracing::field::Empty,
);
let metrics = crate::telemetry_metrics::TelemetryMetrics::default();
let metrics = TelemetryMetrics::default();
let query_metrics =
DbQueryMetricsContext::new(span.clone(), "SELECT".to_string(), "sqlite", &metrics);
query_metrics.record_success(3);
@@ -1035,7 +1035,7 @@ mod tests {
db.response.returned_rows = tracing::field::Empty,
);
let error = anyhow!("query failed").context("while executing SELECT 1");
let metrics = crate::telemetry_metrics::TelemetryMetrics::default();
let metrics = TelemetryMetrics::default();
let query_metrics =
DbQueryMetricsContext::new(span.clone(), "SELECT".to_string(), "sqlite", &metrics);
query_metrics.record_error(2, &error);
+1 -1
View File
@@ -186,7 +186,7 @@ fn extract_set_variable(statement: &mut Statement, database: &DbInfo) -> Option<
)
}
fn syntax_error(error: ParserError, parser: &Parser, sql: &str) -> FileStatement {
fn syntax_error(error: ParserError, parser: &Parser<'_>, sql: &str) -> FileStatement {
let Span {
start: Location {
line: start_line,
+1 -1
View File
@@ -443,7 +443,7 @@ fn has_single_row_shape(statement: &SqlStatement) -> bool {
&& select.from.is_empty()
&& select.lateral_views.is_empty()
&& select.selection.is_none()
&& select.group_by == sqlparser::ast::GroupByExpr::Expressions(vec![], vec![])
&& select.group_by == GroupByExpr::Expressions(vec![], vec![])
&& select.cluster_by.is_empty()
&& select.distribute_by.is_empty()
&& select.sort_by.is_empty()
+1 -1
View File
@@ -55,7 +55,7 @@ pub(in crate::webserver::database) struct DatabaseQuery {
impl DatabaseQuery {
/// Whether row evaluation needs the request's existing connection and
/// must therefore wait until the database stream is closed.
pub fn must_buffer_rows(&self) -> bool {
pub(crate) fn must_buffer_rows(&self) -> bool {
self.computed_columns
.iter()
.any(|column| column.value.contains_function(SqlPageFunctionName::run_sql))
+10 -10
View File
@@ -31,7 +31,7 @@ use sqlx::types::Type;
use sqlx::value::ValueRef;
#[cfg(test)]
pub fn row_to_json(row: &AnyRow) -> Value {
pub(super) fn row_to_json(row: &AnyRow) -> Value {
use Value::Object;
let columns = row.columns();
@@ -48,7 +48,7 @@ pub fn row_to_json(row: &AnyRow) -> Value {
///
/// Every SQL value is decoded exactly once. Private values are addressed by
/// ordinal, so their generated SQL aliases cannot collide with user columns.
pub fn row_to_json_with_inputs(
pub(super) fn row_to_json_with_inputs(
row: &AnyRow,
input_count: usize,
) -> anyhow::Result<(Value, Vec<Value>)> {
@@ -84,7 +84,7 @@ fn canonical_col_name(col: &AnyColumn) -> String {
}
}
pub fn sql_to_json(row: &AnyRow, col: &sqlx::any::AnyColumn) -> Value {
pub(super) fn sql_to_json(row: &AnyRow, col: &AnyColumn) -> Value {
let raw_value_result = row.try_get_raw(col.ordinal());
match raw_value_result {
Ok(raw_value) if !raw_value.is_null() => {
@@ -122,13 +122,13 @@ fn decode_pg_range<'r, T>(raw_value: sqlx::any::AnyValueRef<'r>) -> Value
where
T: std::fmt::Display
+ Type<sqlx::postgres::Postgres>
+ for<'a> sqlx::decode::Decode<'a, sqlx::postgres::Postgres>,
+ for<'a> Decode<'a, sqlx::postgres::Postgres>,
{
let Ok(pg_val): Result<PgValueRef<'r>, _> = raw_value.try_into() else {
log::error!("Only postgres range values are supported");
return Value::Null;
};
match <PgRange<T> as sqlx::decode::Decode<'r, sqlx::postgres::Postgres>>::decode(pg_val) {
match <PgRange<T> as Decode<'r, sqlx::postgres::Postgres>>::decode(pg_val) {
Ok(pg_range) => pg_range.to_string().into(),
Err(e) => {
log::error!("Failed to decode postgres range value: {e}");
@@ -144,7 +144,9 @@ fn decimal_to_json(decimal: &BigDecimal) -> Value {
))
}
pub fn sql_nonnull_to_json<'r>(mut get_ref: impl FnMut() -> sqlx::any::AnyValueRef<'r>) -> Value {
pub(super) fn sql_nonnull_to_json<'r>(
mut get_ref: impl FnMut() -> sqlx::any::AnyValueRef<'r>,
) -> Value {
use AnyTypeInfoKind::{Mssql, MySql};
let raw_value = get_ref();
let type_info = raw_value.type_info();
@@ -168,9 +170,7 @@ pub fn sql_nonnull_to_json<'r>(mut get_ref: impl FnMut() -> sqlx::any::AnyValueR
decode_raw::<bool>(raw_value).into()
}
"BIT" if matches!(db_type, MySql(_)) => decode_raw::<u64>(raw_value).into(),
"DATE" => decode_raw::<chrono::NaiveDate>(raw_value)
.to_string()
.into(),
"DATE" => decode_raw::<NaiveDate>(raw_value).to_string().into(),
"TIME" | "TIMETZ" => decode_raw::<chrono::NaiveTime>(raw_value)
.to_string()
.into(),
@@ -727,7 +727,7 @@ line2' as multiline_string
let expected_json = serde_json::json!({
"null_col": null,
"empty_string": if empty_str_is_null { serde_json::Value::Null } else { serde_json::Value::String(String::new()) },
"empty_string": if empty_str_is_null { Value::Null } else { Value::String(String::new()) },
"zero_value": 0,
"negative_int": -42,
"my_float": 1.23456,
@@ -233,7 +233,7 @@ macro_rules! sqlpage_functions {
/// One variant per built-in `sqlpage.*` function.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[allow(non_camel_case_types)]
pub enum SqlPageFunctionName {
pub(crate) enum SqlPageFunctionName {
$($func),*
}
@@ -308,14 +308,14 @@ fn resolve_bodies(
})?,
)
} else {
body_html.map(std::string::ToString::to_string)
body_html.map(ToString::to_string)
};
// If body is provided it takes precedence; otherwise the raw markdown is used.
let text_body = match body {
Some(body) => body.into_owned(),
None => body_md
.map(std::string::ToString::to_string)
.map(ToString::to_string)
.expect("body_md is present when body is None"),
};
Ok((text_body, html_body))
@@ -64,7 +64,7 @@ pub(super) async fn user_info<'a>(
"gender" => claims.gender().map(|g| g.to_string()), // Assumes GenderClaim impls ToString
"birthdate" => claims.birthdate().map(|b| b.to_string()), // Assumes Birthdate impls ToString
"zoneinfo" => claims.zoneinfo().map(|z| z.to_string()), // Assumes ZoneInfo impls ToString
"locale" => claims.locale().map(std::string::ToString::to_string), // Assumes Locale impls ToString
"locale" => claims.locale().map(ToString::to_string), // Assumes Locale impls ToString
"updated_at" => claims.updated_at().map(|t| t.timestamp().to_string()),
// Standard Claims (Email Scope)
@@ -78,7 +78,7 @@ pub(super) async fn user_info<'a>(
.additional_claims()
.0
.get(additional_claim)
.map(std::string::ToString::to_string),
.map(ToString::to_string),
};
Ok(claim_value_str)
@@ -49,7 +49,7 @@ fn deserialize_map_to_vec_pairs<'de, D: serde::Deserializer<'de>>(
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Vec<(Cow<'de, str>, Cow<'de, str>)>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a map")
}
@@ -14,7 +14,7 @@ impl URLParameters {
}
fn encode_and_push(&mut self, v: &str) {
let val: Cow<str> = percent_encode(v.as_bytes(), NON_ALPHANUMERIC).into();
let val: Cow<'_, str> = percent_encode(v.as_bytes(), NON_ALPHANUMERIC).into();
self.0.push_str(&val);
}
@@ -59,7 +59,7 @@ impl URLParameters {
}
fn add_from_json(&mut self, key: &str, raw_json_value: &str) {
if let Ok(str_val) = serde_json::from_str::<Option<Cow<str>>>(raw_json_value) {
if let Ok(str_val) = serde_json::from_str::<Option<Cow<'_, str>>>(raw_json_value) {
if let Some(str_val) = str_val {
self.push_kv(key, &str_val);
}
@@ -98,7 +98,7 @@ impl<'de> Deserialize<'de> for URLParameters {
impl<'de> serde::de::Visitor<'de> for URLParametersVisitor {
type Value = URLParameters;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a sequence")
}
@@ -108,7 +108,7 @@ impl<'de> Deserialize<'de> for URLParameters {
{
let mut out = URLParameters(String::new());
while let Some((key, value)) =
map.next_entry::<Cow<str>, Cow<serde_json::value::RawValue>>()?
map.next_entry::<Cow<'_, str>, Cow<'_, serde_json::value::RawValue>>()?
{
out.add_from_json(&key, value.get());
}
@@ -121,8 +121,8 @@ impl<'de> Deserialize<'de> for URLParameters {
}
}
impl std::fmt::Display for URLParameters {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for URLParameters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
+1 -1
View File
@@ -240,7 +240,7 @@ pub(super) fn handle_form_error(
_req: &HttpRequest,
) -> actix_web::Error {
match decode_err {
actix_web::error::UrlencodedError::Overflow { size, limit } => {
UrlencodedError::Overflow { size, limit } => {
actix_web::error::ErrorPayloadTooLarge(format!(
"The submitted form data size ({size} bytes) exceeds the maximum allowed upload size ({limit} bytes). \
You can increase this limit by setting max_uploaded_file_size in the configuration file.",
+8 -12
View File
@@ -177,7 +177,7 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
renderer,
} => {
let body_stream = tokio_stream::wrappers::ReceiverStream::new(receiver);
let result_stream = body_stream.map(Ok::<_, actix_web::Error>);
let result_stream = body_stream.map(Ok::<_, Error>);
let http_response = http_response.streaming(result_stream);
return Ok(ResponseWithWriter::RenderStream {
http_response,
@@ -262,7 +262,7 @@ async fn render_sql(
otel.name = %sql_execution_span_name(&source_path),
{ otel::CODE_FILE_PATH } = %source_path.display(),
);
actix_web::rt::spawn(tracing::Instrument::instrument(
actix_web::rt::spawn(Instrument::instrument(
async move {
let request_info = exec_ctx.request();
let request_context = RequestContext {
@@ -291,7 +291,7 @@ async fn render_sql(
resp_send
.send(http_response)
.unwrap_or_else(|e| log::error!("could not send headers {e:?}"));
tracing::Instrument::instrument(
Instrument::instrument(
stream_response(database_entries_stream, renderer),
tracing::info_span!("render"),
)
@@ -326,7 +326,7 @@ fn sql_execution_span_name(source_path: &std::path::Path) -> String {
format!("SQL {}", source_path.display())
}
struct RequestHeaderCarrier<'a>(&'a actix_web::http::header::HeaderMap);
struct RequestHeaderCarrier<'a>(&'a header::HeaderMap);
impl opentelemetry::propagation::Extractor for RequestHeaderCarrier<'_> {
fn get(&self, key: &str) -> Option<&str> {
@@ -334,10 +334,7 @@ impl opentelemetry::propagation::Extractor for RequestHeaderCarrier<'_> {
}
fn keys(&self) -> Vec<&str> {
self.0
.keys()
.map(actix_web::http::header::HeaderName::as_str)
.collect()
self.0.keys().map(header::HeaderName::as_str).collect()
}
}
@@ -386,7 +383,7 @@ impl RootSpanBuilder for SqlPageRootSpanBuilder {
{ otel::EXCEPTION_MESSAGE } = tracing::field::Empty,
"sqlpage.exception.details" = tracing::field::Empty,
);
std::mem::drop(connection_info);
drop(connection_info);
set_otel_parent(request, &span);
span
}
@@ -502,8 +499,7 @@ pub async fn main_handler(
};
match routing_action {
NotFound => {
let accept_header =
header::Accept::parse(&service_request).unwrap_or(header::Accept::star());
let accept_header = Accept::parse(&service_request).unwrap_or(Accept::star());
let prefers_html = accept_header.iter().any(|h| h.item.subtype() == "html");
if prefers_html {
@@ -566,7 +562,7 @@ pub fn create_app(
Response = ServiceResponse<
impl MessageBody<Error = impl std::fmt::Display + std::fmt::Debug>,
>,
Error = actix_web::Error,
Error = Error,
InitError = (),
>,
> {
+6 -8
View File
@@ -45,7 +45,7 @@ pub struct RequestInfo {
pub app_state: Arc<AppState>,
pub raw_body: Option<Vec<u8>>,
pub oidc_claims: Option<OidcClaims>,
pub server_timing: Arc<super::server_timing::ServerTiming>,
pub server_timing: Arc<ServerTiming>,
}
#[derive(Debug)]
@@ -161,7 +161,7 @@ pub(crate) async fn extract_request_info(
}
async fn extract_post_data(
http_req: &mut actix_web::HttpRequest,
http_req: &mut HttpRequest,
payload: &mut actix_web::dev::Payload,
config: &crate::app_config::AppConfig,
) -> anyhow::Result<(
@@ -181,7 +181,7 @@ async fn extract_post_data(
let (vars, files) = extract_multipart_post_data(http_req, payload, config).await?;
Ok((vars, files, None))
} else {
let body = actix_web::web::Bytes::from_request(http_req, payload)
let body = web::Bytes::from_request(http_req, payload)
.await
.with_actix_error_status()
.context("could not read the request body")?;
@@ -194,7 +194,7 @@ async fn extract_post_data(
}
async fn extract_urlencoded_post_variables(
http_req: &mut actix_web::HttpRequest,
http_req: &mut HttpRequest,
payload: &mut actix_web::dev::Payload,
) -> anyhow::Result<Vec<(String, String)>> {
Form::<Vec<(String, String)>>::from_request(http_req, payload)
@@ -205,7 +205,7 @@ async fn extract_urlencoded_post_variables(
}
async fn extract_multipart_post_data(
http_req: &mut actix_web::HttpRequest,
http_req: &mut HttpRequest,
payload: &mut actix_web::dev::Payload,
config: &crate::app_config::AppConfig,
) -> anyhow::Result<(Vec<(String, String)>, Vec<(String, TempFile)>)> {
@@ -297,9 +297,7 @@ async fn extract_file(
/// file upload form fields that are left blank result in the browser sending an empty file, with a mime type of application/octet-stream.
/// We don't want to treat this the same as actual empty files, so we check for this case.
async fn is_file_field_empty(
uploaded_file: &actix_multipart::form::tempfile::TempFile,
) -> anyhow::Result<bool> {
async fn is_file_field_empty(uploaded_file: &TempFile) -> anyhow::Result<bool> {
Ok(
uploaded_file.content_type == Some(mime_guess::mime::APPLICATION_OCTET_STREAM)
&& uploaded_file.file_name.as_deref().is_none_or(str::is_empty)
+1 -1
View File
@@ -3,7 +3,7 @@ use tokio_stream::StreamExt;
use crate::app_config::AppConfig;
pub fn make_auto_rustls_config(domain: &str, config: &AppConfig) -> ServerConfig {
pub(super) fn make_auto_rustls_config(domain: &str, config: &AppConfig) -> ServerConfig {
log::info!("Starting HTTPS configuration for {domain}");
let mut state = AcmeConfig::new([domain])
.contact([if let Some(email) = &config.https_certificate_email {
+2 -2
View File
@@ -10,11 +10,11 @@ type LambdaResponse = Response<Full<Bytes>>;
type LambdaResult = Result<LambdaResponse, lambda_http::Error>;
type LambdaRequest = (Request, oneshot::Sender<LambdaResult>);
pub fn is_running_on_lambda() -> bool {
pub(super) fn is_running_on_lambda() -> bool {
std::env::var_os("AWS_LAMBDA_RUNTIME_API").is_some()
}
pub async fn run<F, I, S, B>(factory: F) -> Result<(), lambda_http::Error>
pub(super) async fn run<F, I, S, B>(factory: F) -> Result<(), lambda_http::Error>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, actix_http::Request>,
+8 -9
View File
@@ -66,12 +66,11 @@ pub struct OidcAdditionalClaims(pub(crate) serde_json::Map<String, serde_json::V
impl openidconnect::AdditionalClaims for OidcAdditionalClaims {}
type OidcToken = openidconnect::IdToken<
OidcAdditionalClaims,
openidconnect::core::CoreGenderClaim,
openidconnect::core::CoreJweContentEncryptionAlgorithm,
openidconnect::core::CoreJwsSigningAlgorithm,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
>;
pub type OidcClaims =
openidconnect::IdTokenClaims<OidcAdditionalClaims, openidconnect::core::CoreGenderClaim>;
pub type OidcClaims = openidconnect::IdTokenClaims<OidcAdditionalClaims, CoreGenderClaim>;
#[derive(Clone, Debug)]
pub struct OidcConfig {
@@ -395,7 +394,7 @@ impl OidcMiddleware {
}
async fn discover_provider_metadata(
http_client: &awc::Client,
http_client: &Client,
issuer_url: IssuerUrl,
) -> anyhow::Result<ProviderMetadataWithLogout> {
log::debug!("Discovering provider metadata for {issuer_url}");
@@ -800,7 +799,7 @@ async fn process_oidc_callback(
async fn exchange_code_for_token(
oidc_client: &OidcClient,
http_client: &awc::Client,
http_client: &Client,
oidc_callback_params: OidcCallbackParams,
) -> anyhow::Result<OidcToken> {
let span = tracing::info_span!(
@@ -936,12 +935,12 @@ fn get_authenticated_user_info(
}
pub struct AwcHttpClient<'c> {
client: &'c awc::Client,
client: &'c Client,
}
impl<'c> AwcHttpClient<'c> {
#[must_use]
pub fn from_client(client: &'c awc::Client) -> Self {
pub fn from_client(client: &'c Client) -> Self {
Self { client }
}
}
+3 -3
View File
@@ -50,7 +50,7 @@ impl ResponseWriter {
.reserve()
.await
.map_err(|_| std::io::ErrorKind::WouldBlock)?;
sender.send(std::mem::take(&mut self.buffer).into());
sender.send(mem::take(&mut self.buffer).into());
Ok(())
}
}
@@ -122,7 +122,7 @@ impl tokio::io::AsyncWrite for AsyncResponseWriter {
} = self.get_mut();
match poll_sender.poll_reserve(cx) {
std::task::Poll::Ready(Ok(())) => {
let res = poll_sender.send_item(std::mem::take(&mut writer.buffer).into());
let res = poll_sender.send_item(mem::take(&mut writer.buffer).into());
std::task::Poll::Ready(res.map_err(|_| std::io::ErrorKind::BrokenPipe.into()))
}
std::task::Poll::Pending => std::task::Poll::Pending,
@@ -142,7 +142,7 @@ impl tokio::io::AsyncWrite for AsyncResponseWriter {
impl Drop for ResponseWriter {
fn drop(&mut self) {
if let Err(e) = std::io::Write::flush(self) {
if let Err(e) = Write::flush(self) {
log::debug!("Could not flush data to client: {e}");
}
}
+1 -2
View File
@@ -115,7 +115,7 @@ pub(crate) struct AppFileStore<'a> {
}
impl<'a> AppFileStore<'a> {
pub fn new(
pub(crate) fn new(
cache: &'a FileCache<SqlFile>,
filesystem: &'a FileSystem,
app_state: &'a AppState,
@@ -637,7 +637,6 @@ mod tests {
fn contains(&self, path: &str) -> bool {
let normalized_path = path.replace('\\', "/");
dbg!(&normalized_path, &self.contents);
self.contents.contains(&normalized_path)
}
+5 -5
View File
@@ -32,26 +32,26 @@ macro_rules! static_file_endpoint {
}
#[must_use]
pub fn js() -> Resource {
pub(super) fn js() -> Resource {
static_file_endpoint!("sqlpage", "js", "application/javascript")
}
#[must_use]
pub fn apexcharts_js() -> Resource {
pub(super) fn apexcharts_js() -> Resource {
static_file_endpoint!("apexcharts", "js", "application/javascript")
}
#[must_use]
pub fn tomselect_js() -> Resource {
pub(super) fn tomselect_js() -> Resource {
static_file_endpoint!("tomselect", "js", "application/javascript")
}
#[must_use]
pub fn css() -> Resource {
pub(super) fn css() -> Resource {
static_file_endpoint!("sqlpage", "css", "text/css")
}
#[must_use]
pub fn favicon() -> Resource {
pub(super) fn favicon() -> Resource {
static_file_endpoint!("favicon", "svg", "image/svg+xml")
}
+18 -17
View File
@@ -1,3 +1,4 @@
use std::fmt::Write as _;
use std::time::Duration;
use actix_web::{
@@ -5,7 +6,7 @@ use actix_web::{
dev::{ServiceRequest, fn_service},
http::header,
http::header::ContentType,
test::{self, TestRequest},
test::TestRequest,
web,
web::Data,
};
@@ -18,11 +19,11 @@ use sqlpage::{
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
pub async fn get_request_to_with_data(
pub(crate) async fn get_request_to_with_data(
path: &str,
data: Data<AppState>,
) -> actix_web::Result<TestRequest> {
Ok(test::TestRequest::get()
Ok(TestRequest::get()
.uri(path)
.insert_header(ContentType::plaintext())
.insert_header(header::Accept::html())
@@ -31,23 +32,23 @@ pub async fn get_request_to_with_data(
.app_data(data))
}
pub async fn get_request_to(path: &str) -> actix_web::Result<TestRequest> {
pub(crate) async fn get_request_to(path: &str) -> actix_web::Result<TestRequest> {
let data = make_app_data().await;
get_request_to_with_data(path, data).await
}
pub async fn make_app_data_from_config(config: AppConfig) -> Data<AppState> {
pub(crate) async fn make_app_data_from_config(config: AppConfig) -> Data<AppState> {
let state = AppState::init(&config).await.unwrap();
Data::new(state)
}
pub async fn make_app_data() -> Data<AppState> {
pub(crate) async fn make_app_data() -> Data<AppState> {
init_log();
let config = test_config();
make_app_data_from_config(config).await
}
pub async fn req_path(
pub(crate) async fn req_path(
path: impl AsRef<str>,
) -> Result<actix_web::dev::ServiceResponse, actix_web::Error> {
let req = get_request_to(path.as_ref()).await?.to_srv_request();
@@ -55,14 +56,14 @@ pub async fn req_path(
}
const REQ_TIMEOUT: Duration = Duration::from_secs(8);
pub async fn req_path_with_app_data(
pub(crate) async fn req_path_with_app_data(
path: impl AsRef<str>,
app_data: Data<AppState>,
) -> anyhow::Result<actix_web::dev::ServiceResponse> {
req_path_with_app_data_and_accept(path, app_data, header::Accept::html()).await
}
pub async fn req_path_with_app_data_json(
pub(crate) async fn req_path_with_app_data_json(
path: impl AsRef<str>,
app_data: Data<AppState>,
) -> anyhow::Result<actix_web::dev::ServiceResponse> {
@@ -75,7 +76,7 @@ async fn req_path_with_app_data_and_accept(
accept: header::Accept,
) -> anyhow::Result<actix_web::dev::ServiceResponse> {
let path = path.as_ref();
let req = test::TestRequest::get()
let req = TestRequest::get()
.uri(path)
.app_data(app_data)
.insert_header(("cookie", "test_cook=123"))
@@ -94,7 +95,7 @@ async fn req_path_with_app_data_and_accept(
Ok(resp)
}
pub fn test_config() -> AppConfig {
pub(crate) fn test_config() -> AppConfig {
let db_url = test_database_url();
serde_json::from_str::<AppConfig>(&format!(
r#"{{
@@ -111,7 +112,7 @@ pub fn test_config() -> AppConfig {
.unwrap()
}
pub fn init_log() {
pub(crate) fn init_log() {
telemetry::init_test_logging();
}
@@ -123,7 +124,7 @@ fn format_request_line_and_headers(req: &ServiceRequest) -> String {
if k.as_str().eq_ignore_ascii_case("date") {
continue;
}
out.push_str(&format!("|{k}: {}", v.to_str().unwrap_or("?")));
write!(out, "|{k}: {}", v.to_str().unwrap_or("?")).unwrap();
}
out
}
@@ -135,24 +136,24 @@ async fn format_body(req: &mut ServiceRequest) -> Vec<u8> {
.unwrap_or_default()
}
fn build_echo_response(body: Vec<u8>, meta: String) -> HttpResponse {
fn build_echo_response(body: &[u8], meta: String) -> HttpResponse {
let mut resp = meta.into_bytes();
resp.push(b'|');
resp.extend_from_slice(&body);
resp.extend_from_slice(body);
HttpResponse::Ok()
.insert_header((header::DATE, "Mon, 24 Feb 2025 12:00:00 GMT"))
.insert_header((header::CONTENT_TYPE, "text/plain"))
.body(resp)
}
pub fn start_echo_server(shutdown: oneshot::Receiver<()>) -> (JoinHandle<()>, u16) {
pub(crate) fn start_echo_server(shutdown: oneshot::Receiver<()>) -> (JoinHandle<()>, u16) {
let listener = std::net::TcpListener::bind("localhost:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = HttpServer::new(|| {
App::new().default_service(fn_service(|mut req: ServiceRequest| async move {
let meta = format_request_line_and_headers(&req);
let body = format_body(&mut req).await;
let resp = build_echo_response(body, meta);
let resp = build_echo_response(&body, meta);
Ok(req.into_response(resp))
}))
})
+5 -5
View File
@@ -23,7 +23,7 @@ async fn test_concurrent_requests() {
})
.collect::<Vec<_>>();
let results = futures_util::future::join_all(reqs).await;
for result in results.into_iter() {
for result in results {
let resp = result.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
@@ -52,7 +52,7 @@ async fn test_routing_with_db_fs() {
if matches!(
state.db.info.database_type,
sqlpage::webserver::database::SupportedDatabase::Oracle
webserver::database::SupportedDatabase::Oracle
) {
return;
}
@@ -116,7 +116,7 @@ async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() {
(&mut *conn)
.execute(sqlpage::filesystem::DbFsQueries::get_create_table_sql(
sqlpage::webserver::database::SupportedDatabase::Sqlite,
webserver::database::SupportedDatabase::Sqlite,
))
.await
.unwrap();
@@ -140,7 +140,7 @@ async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() {
.app_data(app_data)
.to_srv_request();
let err = sqlpage::webserver::http::main_handler(req)
let err = webserver::http::main_handler(req)
.await
.expect_err("non-unicode path should not panic and must return bad request");
assert_eq!(
@@ -213,7 +213,7 @@ async fn test_hidden_files() {
);
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let srv_resp = actix_web::test::TestRequest::default().to_srv_response(resp);
let srv_resp = test::TestRequest::default().to_srv_response(resp);
let body = test::read_body(srv_resp).await;
assert!(
String::from_utf8_lossy(&body)
+2 -2
View File
@@ -109,7 +109,7 @@ async fn test_csv_filename_header_injection() -> actix_web::Result<()> {
#[actix_web::test]
async fn test_json_columns() {
let app_data = crate::common::make_app_data().await;
let app_data = make_app_data().await;
if !matches!(
app_data.db.to_string().to_lowercase().as_str(),
"postgres" | "sqlite"
@@ -135,7 +135,7 @@ async fn test_json_columns() {
"the json should have been parsed, not returned as a string, in: {body_html_escaped}"
);
assert!(
!body_html_escaped.contains("{"),
!body_html_escaped.contains('{'),
"the json should have been parsed, not returned as a string, in: {body_html_escaped}"
);
}
+7 -7
View File
@@ -14,7 +14,7 @@ async fn direct_request_status(path: &str, app_data: actix_web::web::Data<AppSta
let req = test::TestRequest::get()
.uri(path)
.app_data(app_data)
.insert_header(actix_web::http::header::Accept::html())
.insert_header(http::header::Accept::html())
.to_srv_request();
match main_handler(req).await {
Ok(resp) => resp.status(),
@@ -72,7 +72,7 @@ async fn test_privileged_paths_are_not_accessible() {
);
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let srv_resp = actix_web::test::TestRequest::default().to_srv_response(resp);
let srv_resp = test::TestRequest::default().to_srv_response(resp);
let body = test::read_body(srv_resp).await;
assert!(
String::from_utf8_lossy(&body)
@@ -90,7 +90,7 @@ async fn test_404_fallback() {
] {
let resp_result = req_path(f).await;
let resp = resp_result.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK, "{f} isnt 200");
assert_eq!(resp.status(), StatusCode::OK, "{f} isnt 200");
let body = test::read_body(resp).await;
assert!(body.starts_with(b"<!DOCTYPE html>"));
@@ -113,7 +113,7 @@ async fn test_default_404() {
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
StatusCode::NOT_FOUND,
"{f} should return 404"
);
@@ -135,7 +135,7 @@ async fn test_default_404_with_redirect() {
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
StatusCode::NOT_FOUND,
"/i-do-not-exist should return 404"
);
@@ -143,7 +143,7 @@ async fn test_default_404_with_redirect() {
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
StatusCode::NOT_FOUND,
"/i-do-not-exist/ should return 404"
);
@@ -164,7 +164,7 @@ async fn test_default_404_when_request_path_descends_into_file() {
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
StatusCode::NOT_FOUND,
"descending into a file path should behave like a missing resource"
);
+25 -27
View File
@@ -19,7 +19,7 @@ fn base64url_encode(data: &[u8]) -> String {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
}
pub fn make_jwt(claims: &serde_json::Value, secret: &str) -> String {
pub(crate) fn make_jwt(claims: &serde_json::Value, secret: &str) -> String {
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
@@ -32,7 +32,7 @@ pub fn make_jwt(claims: &serde_json::Value, secret: &str) -> String {
let header_b64 = base64url_encode(header.to_string().as_bytes());
let payload_b64 = base64url_encode(claims.to_string().as_bytes());
let message = format!("{}.{}", header_b64, payload_b64);
let message = format!("{header_b64}.{payload_b64}");
let mut mac =
Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC accepts any key size");
@@ -40,7 +40,7 @@ pub fn make_jwt(claims: &serde_json::Value, secret: &str) -> String {
let signature = mac.finalize().into_bytes();
let signature_b64 = base64url_encode(&signature);
format!("{}.{}.{}", header_b64, payload_b64, signature_b64)
format!("{header_b64}.{payload_b64}.{signature_b64}")
}
type JwtCustomizer<'a> = dyn Fn(serde_json::Value, &str) -> String + Send + Sync + 'a;
@@ -141,11 +141,10 @@ async fn token_endpoint(
"nonce": nonce,
});
let id_token = state
.jwt_customizer
.take()
.map(|customizer| customizer(claims.clone(), &state.secret))
.unwrap_or_else(|| make_jwt(&claims, &state.secret));
let id_token = state.jwt_customizer.take().map_or_else(
|| make_jwt(&claims, &state.secret),
|customizer| customizer(claims.clone(), &state.secret),
);
let delay = state.token_endpoint_delay;
drop(state);
@@ -167,7 +166,7 @@ async fn token_endpoint(
.streaming(body)
}
pub struct FakeOidcProvider {
pub(crate) struct FakeOidcProvider {
pub issuer_url: String,
pub client_id: String,
pub client_secret: String,
@@ -185,10 +184,10 @@ fn extract_set_cookies(headers: &header::HeaderMap) -> Vec<Cookie<'static>> {
}
impl FakeOidcProvider {
pub async fn new() -> Self {
pub(crate) fn new() -> Self {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let issuer_url = format!("http://127.0.0.1:{}", port);
let issuer_url = format!("http://127.0.0.1:{port}");
let client_id = "test_client".to_string();
let client_secret = "test_secret".to_string();
@@ -236,20 +235,20 @@ impl FakeOidcProvider {
}
}
fn with_state_mut<R>(&self, f: impl FnOnce(&mut ProviderState) -> R) -> R {
fn with_state_mut<R>(&self, f: impl FnOnce(&mut ProviderState<'_>) -> R) -> R {
let mut state = self.state.lock().unwrap();
f(&mut state)
}
pub fn set_token_endpoint_delay(&self, delay: Duration) {
pub(crate) fn set_token_endpoint_delay(&self, delay: Duration) {
self.with_state_mut(|s| s.token_endpoint_delay = delay);
}
pub fn discovery_count(&self) -> usize {
pub(crate) fn discovery_count(&self) -> usize {
self.state.lock().unwrap().discovery_count
}
pub fn store_auth_code(&self, code: String, nonce: String) {
pub(crate) fn store_auth_code(&self, code: String, nonce: String) {
self.with_state_mut(|s| {
s.auth_codes.insert(code, nonce);
});
@@ -283,7 +282,7 @@ macro_rules! request_with_cookies {
}
let resp = test::call_service(&$app, req.to_request()).await;
for new_cookie in extract_set_cookies(resp.headers()) {
$cookies.retain(|c: &Cookie| c.name() != new_cookie.name());
$cookies.retain(|c: &Cookie<'_>| c.name() != new_cookie.name());
if !new_cookie.value().is_empty() {
$cookies.push(new_cookie);
}
@@ -293,7 +292,7 @@ macro_rules! request_with_cookies {
}
async fn setup_oidc_test(
provider_mutator: impl FnOnce(&mut ProviderState),
provider_mutator: impl FnOnce(&mut ProviderState<'_>),
) -> (
impl actix_web::dev::Service<
actix_http::Request,
@@ -307,7 +306,7 @@ async fn setup_oidc_test(
app_config::{AppConfig, test_database_url},
};
crate::common::init_log();
let provider = FakeOidcProvider::new().await;
let provider = FakeOidcProvider::new();
provider.with_state_mut(provider_mutator);
let db_url = test_database_url();
@@ -436,7 +435,7 @@ async fn test_oidc_happy_path() {
}
async fn assert_oidc_login_fails(
provider_mutator: impl FnOnce(&mut ProviderState),
provider_mutator: impl FnOnce(&mut ProviderState<'_>),
state_override: Option<String>,
) {
let (app, provider) = setup_oidc_test(provider_mutator).await;
@@ -545,7 +544,7 @@ async fn test_oidc_expired_token_is_rejected() {
}
async fn setup_oidc_test_with_prefix(
provider_mutator: impl FnOnce(&mut ProviderState),
provider_mutator: impl FnOnce(&mut ProviderState<'_>),
site_prefix: &str,
) -> (
impl actix_web::dev::Service<
@@ -560,7 +559,7 @@ async fn setup_oidc_test_with_prefix(
app_config::{AppConfig, test_database_url},
};
crate::common::init_log();
let provider = FakeOidcProvider::new().await;
let provider = FakeOidcProvider::new();
provider.with_state_mut(provider_mutator);
let db_url = test_database_url();
@@ -596,8 +595,7 @@ async fn test_oidc_with_site_prefix() {
let redirect_uri = get_query_param(&auth_url, "redirect_uri");
assert!(
redirect_uri.contains("/my-app/sqlpage/oidc_callback"),
"Redirect URI should contain site prefix. Got: {}",
redirect_uri
"Redirect URI should contain site prefix. Got: {redirect_uri}"
);
}
@@ -609,7 +607,7 @@ async fn test_oidc_logout_uses_correct_scheme() {
};
crate::common::init_log();
let provider = FakeOidcProvider::new().await;
let provider = FakeOidcProvider::new();
let db_url = test_database_url();
let config_json = format!(
@@ -718,7 +716,7 @@ async fn test_slow_token_endpoint_does_not_freeze_server() {
let handle = tokio::task::spawn_local(async move {
let mut req = test::TestRequest::get().uri(&callback_uri);
for cookie in cookies.iter() {
for cookie in &cookies {
req = req.cookie(cookie.clone());
}
test::call_service(&app, req.to_request()).await
@@ -728,7 +726,7 @@ async fn test_slow_token_endpoint_does_not_freeze_server() {
// then advance past the body-read timeout.
tokio::task::yield_now().await;
tokio::time::pause();
tokio::time::advance(Duration::from_secs(60)).await;
tokio::time::advance(Duration::from_mins(1)).await;
let resp = tokio::time::timeout(Duration::from_secs(1), handle)
.await
@@ -749,7 +747,7 @@ async fn test_oidc_logout_is_session_bound() {
};
crate::common::init_log();
let provider = FakeOidcProvider::new().await;
let provider = FakeOidcProvider::new();
let db_url = test_database_url();
let config_json = format!(
+4 -6
View File
@@ -112,7 +112,7 @@ async fn test_download_data_url() -> actix_web::Result<()> {
#[actix_web::test]
async fn test_large_form_field_roundtrip() -> actix_web::Result<()> {
let long_string = "a".repeat(123454);
let long_string = "a".repeat(123_454);
let req = get_request_to("/tests/components/display_form_field.sql")
.await?
.insert_header(("content-type", "application/x-www-form-urlencoded"))
@@ -193,7 +193,7 @@ async fn test_variables_function() -> actix_web::Result<()> {
assert_eq!(
actual_decoded, expected_value,
"step {i}: {key} mismatch: {actual_decoded:#} != {expected_value:#}"
)
);
}
}
@@ -223,8 +223,7 @@ async fn test_invalid_utf8_multipart_text_field_returns_bad_request() -> actix_w
assert_eq!(
status,
StatusCode::BAD_REQUEST,
"assertion error, expected 400 bad request on invalid utf8 payload, got {}",
status
"assertion error, expected 400 bad request on invalid utf8 payload, got {status}"
);
Ok(())
@@ -252,8 +251,7 @@ async fn test_missing_multipart_content_disposition_returns_bad_request() -> act
assert_eq!(
status,
StatusCode::BAD_REQUEST,
"expected 400 bad request on malformed multipart payload, got {}",
status
"expected 400 bad request on malformed multipart payload, got {status}"
);
Ok(())
+14 -18
View File
@@ -1,5 +1,6 @@
use actix_web::test;
use sqlpage::AppState;
use std::fmt::Write as _;
use std::time::Duration;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
@@ -93,7 +94,7 @@ async fn run_sql_test(
let mut query_params = "x=1".to_string();
if test_file_path.contains("fetch") {
query_params.push_str(&format!("&echo_port={port}"));
write!(query_params, "&echo_port={port}").unwrap();
}
let req_str = format!("/{test_file_path}?{query_params}");
@@ -137,9 +138,8 @@ fn assert_json_test(body: &str, test_file: &std::path::Path) {
);
for row in rows {
let obj = match row.as_object() {
Some(o) => o,
None => continue,
let Some(obj) = row.as_object() else {
continue;
};
if let Some(err) = format_error(obj) {
@@ -172,13 +172,12 @@ fn assert_json_test(body: &str, test_file: &std::path::Path) {
})
.unwrap_or_default();
if expected.is_empty() && expected_contains.is_empty() {
panic!(
"{}: No `expected` column returned: \n{:#}",
test_file.display(),
row
);
}
assert!(
!(expected.is_empty() && expected_contains.is_empty()),
"{}: No `expected` column returned: \n{:#}",
test_file.display(),
row
);
let exact_ok = expected.is_empty() || expected.iter().any(|e| e == &actual);
let contains_ok = expected_contains.is_empty()
@@ -187,16 +186,13 @@ fn assert_json_test(body: &str, test_file: &std::path::Path) {
if !exact_ok || !contains_ok {
let mut msg = format!("Test failed: {}\n", test_file.display());
if !expected.is_empty() {
let expected_strs: Vec<String> = expected.iter().map(|d| d.to_string()).collect();
msg.push_str(&format!("Expected: {}\n", expected_strs.join(" or ")));
let expected_strs: Vec<String> = expected.iter().map(ToString::to_string).collect();
writeln!(msg, "Expected: {}", expected_strs.join(" or ")).unwrap();
}
if !expected_contains.is_empty() {
msg.push_str(&format!(
"Expected to contain: {}\n",
expected_contains.join(", ")
));
writeln!(msg, "Expected to contain: {}", expected_contains.join(", ")).unwrap();
}
msg.push_str(&format!("Actual: {}\n", actual));
writeln!(msg, "Actual: {actual}").unwrap();
panic!("{}", msg);
}
}
+2 -2
View File
@@ -31,7 +31,7 @@ async fn test_file_upload(target: &str) -> actix_web::Result<()> {
#[actix_web::test]
async fn test_persist_uploaded_file_mode() -> actix_web::Result<()> {
let app_data = crate::common::make_app_data().await;
let req = actix_web::test::TestRequest::get()
let req = test::TestRequest::get()
.uri("/tests/uploads/persist_with_mode.sql?mode=644")
.app_data(app_data.clone())
.app_data(sqlpage::webserver::http::payload_config(&app_data))
@@ -132,7 +132,7 @@ async fn test_file_upload_too_large() -> actix_web::Result<()> {
\r\n\
"
.to_string()
+ "a".repeat(123457).as_str()
+ "a".repeat(123_457).as_str()
+ "\r\n\
--1234567890--\r\n",
)