1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-27 17:52:56 +01:00

Add unicode feature to switch between regex and regex-lite crates as a trade-off between full unicode support and binary size (#3291)

* - Add `unicode` feature to switch between `regex` and `regex-lite`

as a trade-off between full unicode support and binary size.

* Update CHANGES.md

* Update CHANGES.md

* refactor: move regexset code selection to own module

* docs: add docs within RegexSet module

* chore: restore manifests

* test: ensure all actix-router codepaths are tested

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
LoveSy 2024-03-03 23:50:16 +08:00 committed by GitHub
parent 994ea45d91
commit c10f05a867
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 112 additions and 20 deletions

View File

@ -70,6 +70,7 @@ jobs:
shell: bash shell: bash
run: | run: |
set -e set -e
cargo test --lib --tests -p=actix-router --no-default-features
cargo test --lib --tests -p=actix-router --all-features cargo test --lib --tests -p=actix-router --all-features
cargo test --lib --tests -p=actix-http --all-features cargo test --lib --tests -p=actix-http --all-features
cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,rustls-0_22,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,rustls-0_22,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls

View File

@ -2,6 +2,7 @@
## Unreleased ## Unreleased
- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.
## 0.5.2 ## 0.5.2

View File

@ -17,12 +17,16 @@ name = "actix_router"
path = "src/lib.rs" path = "src/lib.rs"
[features] [features]
default = ["http"] default = ["http", "unicode"]
http = ["dep:http"]
unicode = ["dep:regex"]
[dependencies] [dependencies]
bytestring = ">=0.1.5, <2" bytestring = ">=0.1.5, <2"
cfg-if = "1"
http = { version = "0.2.7", optional = true } http = { version = "0.2.7", optional = true }
regex = "1.5" regex = { version = "1.5", optional = true }
regex-lite = "0.1"
serde = "1" serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
@ -35,6 +39,7 @@ percent-encoding = "2.1"
[[bench]] [[bench]]
name = "router" name = "router"
harness = false harness = false
required-features = ["unicode"]
[[bench]] [[bench]]
name = "quoter" name = "quoter"

View File

@ -10,6 +10,7 @@ mod de;
mod path; mod path;
mod pattern; mod pattern;
mod quoter; mod quoter;
mod regex_set;
mod resource; mod resource;
mod resource_path; mod resource_path;
mod router; mod router;

View File

@ -0,0 +1,66 @@
//! Abstraction over `regex` and `regex-lite` depending on whether we have `unicode` crate feature
//! enabled.
use cfg_if::cfg_if;
#[cfg(feature = "unicode")]
pub(crate) use regex::{escape, Regex};
#[cfg(not(feature = "unicode"))]
pub(crate) use regex_lite::{escape, Regex};
#[cfg(feature = "unicode")]
#[derive(Debug, Clone)]
pub(crate) struct RegexSet(regex::RegexSet);
#[cfg(not(feature = "unicode"))]
#[derive(Debug, Clone)]
pub(crate) struct RegexSet(Vec<regex_lite::Regex>);
impl RegexSet {
/// Create a new regex set.
///
/// # Panics
///
/// Panics if any path patterns are malformed.
pub(crate) fn new(re_set: Vec<String>) -> Self {
cfg_if! {
if #[cfg(feature = "unicode")] {
Self(regex::RegexSet::new(re_set).unwrap())
} else {
Self(re_set.iter().map(|re| Regex::new(re).unwrap()).collect())
}
}
}
/// Create a new empty regex set.
pub(crate) fn empty() -> Self {
cfg_if! {
if #[cfg(feature = "unicode")] {
Self(regex::RegexSet::empty())
} else {
Self(Vec::new())
}
}
}
/// Returns true if regex set matches `path`.
pub(crate) fn is_match(&self, path: &str) -> bool {
cfg_if! {
if #[cfg(feature = "unicode")] {
self.0.is_match(path)
} else {
self.0.iter().any(|re| re.is_match(path))
}
}
}
/// Returns index within `path` of first match.
pub(crate) fn first_match_idx(&self, path: &str) -> Option<usize> {
cfg_if! {
if #[cfg(feature = "unicode")] {
self.0.matches(path).into_iter().next()
} else {
Some(self.0.iter().enumerate().find(|(_, re)| re.is_match(path))?.0)
}
}
}
}

View File

@ -5,10 +5,13 @@ use std::{
mem, mem,
}; };
use regex::{escape, Regex, RegexSet};
use tracing::error; use tracing::error;
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath}; use crate::{
path::PathItem,
regex_set::{escape, Regex, RegexSet},
IntoPatterns, Patterns, Resource, ResourcePath,
};
const MAX_DYNAMIC_SEGMENTS: usize = 16; const MAX_DYNAMIC_SEGMENTS: usize = 16;
@ -233,7 +236,7 @@ enum PatternSegment {
Var(String), Var(String),
} }
#[derive(Clone, Debug)] #[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
enum PatternType { enum PatternType {
/// Single constant/literal segment. /// Single constant/literal segment.
@ -603,7 +606,7 @@ impl ResourceDef {
PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()),
PatternType::DynamicSet(re, params) => { PatternType::DynamicSet(re, params) => {
let idx = re.matches(path).into_iter().next()?; let idx = re.first_match_idx(path)?;
let (ref pattern, _) = params[idx]; let (ref pattern, _) = params[idx];
Some(pattern.captures(path)?[1].len()) Some(pattern.captures(path)?[1].len())
} }
@ -706,7 +709,7 @@ impl ResourceDef {
PatternType::DynamicSet(re, params) => { PatternType::DynamicSet(re, params) => {
let path = path.unprocessed(); let path = path.unprocessed();
let (pattern, names) = match re.matches(path).into_iter().next() { let (pattern, names) = match re.first_match_idx(path) {
Some(idx) => &params[idx], Some(idx) => &params[idx],
_ => return false, _ => return false,
}; };
@ -870,7 +873,7 @@ impl ResourceDef {
} }
} }
let pattern_re_set = RegexSet::new(re_set).unwrap(); let pattern_re_set = RegexSet::new(re_set);
let segments = segments.unwrap_or_default(); let segments = segments.unwrap_or_default();
( (

View File

@ -4,6 +4,7 @@
### Changed ### Changed
- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.
## 4.5.1 ## 4.5.1

View File

@ -40,7 +40,7 @@ name = "actix_web"
path = "src/lib.rs" path = "src/lib.rs"
[features] [features]
default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "http2"] default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "http2", "unicode"]
# Brotli algorithm content-encoding support # Brotli algorithm content-encoding support
compress-brotli = ["actix-http/compress-brotli", "__compress"] compress-brotli = ["actix-http/compress-brotli", "__compress"]
@ -72,6 +72,9 @@ rustls-0_21 = ["http2", "actix-http/rustls-0_21", "actix-tls/accept", "actix-tls
# TLS via Rustls v0.22 # TLS via Rustls v0.22
rustls-0_22 = ["http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"] rustls-0_22 = ["http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"]
# Full unicode support
unicode = ["dep:regex", "actix-router/unicode"]
# Internal (PRIVATE!) features used to aid testing and checking feature status. # Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
__compress = [] __compress = []
@ -89,7 +92,7 @@ actix-utils = "3"
actix-tls = { version = "3.3", default-features = false, optional = true } actix-tls = { version = "3.3", default-features = false, optional = true }
actix-http = { version = "3.6", features = ["ws"] } actix-http = { version = "3.6", features = ["ws"] }
actix-router = "0.5" actix-router = { version = "0.5", default-features = false, features = ["http"] }
actix-web-codegen = { version = "4.2", optional = true } actix-web-codegen = { version = "4.2", optional = true }
ahash = "0.8" ahash = "0.8"
@ -107,7 +110,8 @@ log = "0.4"
mime = "0.3" mime = "0.3"
once_cell = "1.5" once_cell = "1.5"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
regex = "1.5.5" regex = { version = "1.5.5", optional = true }
regex-lite = "0.1"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"

View File

@ -13,7 +13,10 @@
use std::fmt::{self, Write}; use std::fmt::{self, Write};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
#[cfg(feature = "unicode")]
use regex::Regex; use regex::Regex;
#[cfg(not(feature = "unicode"))]
use regex_lite::Regex;
use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer}; use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
use crate::http::header; use crate::http::header;

View File

@ -18,7 +18,10 @@ use bytes::Bytes;
use futures_core::ready; use futures_core::ready;
use log::{debug, warn}; use log::{debug, warn};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use regex::{Regex, RegexSet}; #[cfg(feature = "unicode")]
use regex::Regex;
#[cfg(not(feature = "unicode"))]
use regex_lite::Regex;
use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use crate::{ use crate::{
@ -87,7 +90,7 @@ pub struct Logger(Rc<Inner>);
struct Inner { struct Inner {
format: Format, format: Format,
exclude: HashSet<String>, exclude: HashSet<String>,
exclude_regex: RegexSet, exclude_regex: Vec<Regex>,
log_target: Cow<'static, str>, log_target: Cow<'static, str>,
} }
@ -97,7 +100,7 @@ impl Logger {
Logger(Rc::new(Inner { Logger(Rc::new(Inner {
format: Format::new(format), format: Format::new(format),
exclude: HashSet::new(), exclude: HashSet::new(),
exclude_regex: RegexSet::empty(), exclude_regex: Vec::new(),
log_target: Cow::Borrowed(module_path!()), log_target: Cow::Borrowed(module_path!()),
})) }))
} }
@ -114,10 +117,7 @@ impl Logger {
/// Ignore and do not log access info for paths that match regex. /// Ignore and do not log access info for paths that match regex.
pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self { pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap(); let inner = Rc::get_mut(&mut self.0).unwrap();
let mut patterns = inner.exclude_regex.patterns().to_vec(); inner.exclude_regex.push(Regex::new(&path.into()).unwrap());
patterns.push(path.into());
let regex_set = RegexSet::new(patterns).unwrap();
inner.exclude_regex = regex_set;
self self
} }
@ -240,7 +240,7 @@ impl Default for Logger {
Logger(Rc::new(Inner { Logger(Rc::new(Inner {
format: Format::default(), format: Format::default(),
exclude: HashSet::new(), exclude: HashSet::new(),
exclude_regex: RegexSet::empty(), exclude_regex: Vec::new(),
log_target: Cow::Borrowed(module_path!()), log_target: Cow::Borrowed(module_path!()),
})) }))
} }
@ -300,7 +300,11 @@ where
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let excluded = self.inner.exclude.contains(req.path()) let excluded = self.inner.exclude.contains(req.path())
|| self.inner.exclude_regex.is_match(req.path()); || self
.inner
.exclude_regex
.iter()
.any(|r| r.is_match(req.path()));
if excluded { if excluded {
LoggerResponse { LoggerResponse {

View File

@ -4,7 +4,10 @@ use actix_http::uri::{PathAndQuery, Uri};
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use actix_utils::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use bytes::Bytes; use bytes::Bytes;
#[cfg(feature = "unicode")]
use regex::Regex; use regex::Regex;
#[cfg(not(feature = "unicode"))]
use regex_lite::Regex;
use crate::{ use crate::{
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},