Skip to content

Commit 3baef0f

Browse files
committed
Improve the semantics of split_off_query_fragment
* if there is a query or fragment portion always format with the 'prepender' `?`/`#` character, even if the part is empty. * if there is a query and a fragment section, don't get confused by `?`s in the fragment, these are allowed by the spec. Furthermore, improve the semantics of `AssetIdent` to both document and enforce these same semantics around the `query` and `fragment` parts.
1 parent 7973ff9 commit 3baef0f

File tree

5 files changed

+125
-51
lines changed

5 files changed

+125
-51
lines changed

turbopack/crates/turbopack-core/src/file_source.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ impl FileSource {
3333
query: ResolvedVc<RcStr>,
3434
) -> Result<Vc<Self>> {
3535
if query.await?.is_empty() {
36+
// Delegate to the other factory to ensure there is a single empty ResolvedVc<RcStr> for
37+
// the FileSource type.
3638
Ok(Self::new(*path))
3739
} else {
3840
Ok(Self::cell(FileSource { path, query }))

turbopack/crates/turbopack-core/src/ident.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ use crate::resolve::ModulePart;
1515
pub struct AssetIdent {
1616
/// The primary path of the asset
1717
pub path: ResolvedVc<FileSystemPath>,
18-
/// The query string of the asset (e.g. `?foo=bar`)
18+
/// The query string of the asset this is either the empty string or a query string that starts
19+
/// with a `?` (e.g. `?foo=bar`)
1920
pub query: ResolvedVc<RcStr>,
20-
/// The fragment of the asset (e.g. `#foo`)
21-
pub fragment: Option<ResolvedVc<RcStr>>,
21+
/// The fragment of the asset, this is either the empty string or a fragment string that starts
22+
/// with a `#` (e.g. `#foo`)
23+
pub fragment: ResolvedVc<RcStr>,
2224
/// The assets that are nested in this asset
2325
pub assets: Vec<(ResolvedVc<RcStr>, ResolvedVc<AssetIdent>)>,
2426
/// The modifiers of this asset (e.g. `client chunks`)
@@ -57,14 +59,10 @@ impl ValueToString for AssetIdent {
5759
async fn to_string(&self) -> Result<Vc<RcStr>> {
5860
let mut s = self.path.to_string().owned().await?.into_owned();
5961

60-
let query = self.query.await?;
61-
if !query.is_empty() {
62-
write!(s, "?{}", &*query)?;
63-
}
64-
65-
if let Some(fragment) = &self.fragment {
66-
write!(s, "#{}", fragment.await?)?;
67-
}
62+
// The query string is either empty or non-empty starting with `?` so we can just concat
63+
s.push_str(&self.query.await?);
64+
// ditto for fragment
65+
s.push_str(&self.fragment.await?);
6866

6967
if !self.assets.is_empty() {
7068
s.push_str(" {");
@@ -121,8 +119,19 @@ impl ValueToString for AssetIdent {
121119
#[turbo_tasks::value_impl]
122120
impl AssetIdent {
123121
#[turbo_tasks::function]
124-
pub fn new(ident: Value<AssetIdent>) -> Vc<Self> {
125-
ident.into_value().cell()
122+
pub async fn new(ident: Value<AssetIdent>) -> Result<Vc<Self>> {
123+
let query = &*ident.query.await?;
124+
// TODO(lukesandber); downgrade to debug_assert
125+
assert!(
126+
query.is_empty() || query.starts_with("?"),
127+
"query should be empty or start with a `?`"
128+
);
129+
let fragment = &*ident.fragment.await?;
130+
assert!(
131+
fragment.is_empty() || fragment.starts_with("#"),
132+
"query should be empty or start with a `?`"
133+
);
134+
Ok(ident.into_value().cell())
126135
}
127136

128137
/// Creates an [AssetIdent] from a [Vc<FileSystemPath>]
@@ -131,7 +140,7 @@ impl AssetIdent {
131140
Self::new(Value::new(AssetIdent {
132141
path,
133142
query: ResolvedVc::cell(RcStr::default()),
134-
fragment: None,
143+
fragment: ResolvedVc::cell(RcStr::default()),
135144
assets: Vec::new(),
136145
modifiers: Vec::new(),
137146
parts: Vec::new(),
@@ -252,9 +261,10 @@ impl AssetIdent {
252261
query.deterministic_hash(&mut hasher);
253262
has_hash = true;
254263
}
255-
if let Some(fragment) = fragment {
264+
let fragment = fragment.await?;
265+
if !fragment.is_empty() {
256266
1_u8.deterministic_hash(&mut hasher);
257-
fragment.await?.deterministic_hash(&mut hasher);
267+
fragment.deterministic_hash(&mut hasher);
258268
has_hash = true;
259269
}
260270
for (key, ident) in assets.iter() {

turbopack/crates/turbopack-core/src/resolve/parse.rs

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -61,30 +61,32 @@ pub enum Request {
6161
},
6262
}
6363

64-
fn split_off_query_fragment(raw: RcStr) -> (Pattern, Vc<RcStr>, Vc<RcStr>) {
65-
let Some((raw, query)) = raw.split_once('?') else {
66-
if let Some((raw, fragment)) = raw.split_once('#') {
67-
return (
68-
Pattern::Constant(raw.into()),
69-
Vc::<RcStr>::default(),
70-
Vc::cell(fragment.into()),
71-
);
64+
/// Splits a string like `foo?bar#baz` into `(Pattern::Constant('foo'), '?bar', '#baz')`
65+
///
66+
/// If the hash or query portion are missing they will be empty strings otherwise they will be
67+
/// non-empty along with their prepender characters
68+
fn split_off_query_fragment(mut raw: &str) -> (Pattern, RcStr, RcStr) {
69+
// Per the URI spec fragments can contain `?` characters, so we should trim it off first
70+
// https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
71+
72+
let hash = match raw.as_bytes().iter().position(|&b| b == b'#') {
73+
Some(pos) => {
74+
let hash = RcStr::from(&raw[pos..]);
75+
raw = &raw[..pos];
76+
hash
7277
}
73-
74-
return (
75-
Pattern::Constant(raw),
76-
Vc::<RcStr>::default(),
77-
Vc::<RcStr>::default(),
78-
);
78+
None => RcStr::default(),
7979
};
8080

81-
let (query, fragment) = query.split_once('#').unwrap_or((query, ""));
82-
83-
(
84-
Pattern::Constant(raw.into()),
85-
Vc::cell(format!("?{query}").into()),
86-
Vc::cell(format!("#{fragment}").into()),
87-
)
81+
let query = match raw.as_bytes().iter().position(|&b| b == b'?') {
82+
Some(pos) => {
83+
let query = RcStr::from(&raw[pos..]);
84+
raw = &raw[..pos];
85+
query
86+
}
87+
None => RcStr::default(),
88+
};
89+
(Pattern::Constant(RcStr::from(raw)), query, hash)
8890
}
8991

9092
lazy_static! {
@@ -164,12 +166,12 @@ impl Request {
164166
}
165167

166168
if r.starts_with('/') {
167-
let (path, query, fragment) = split_off_query_fragment(r);
169+
let (path, query, fragment) = split_off_query_fragment(&r);
168170

169171
return Ok(Request::ServerRelative {
170172
path,
171-
query: query.to_resolved().await?,
172-
fragment: fragment.to_resolved().await?,
173+
query: ResolvedVc::cell(query),
174+
fragment: ResolvedVc::cell(fragment),
173175
});
174176
}
175177

@@ -180,23 +182,23 @@ impl Request {
180182
}
181183

182184
if r.starts_with("./") || r.starts_with("../") || r == "." || r == ".." {
183-
let (path, query, fragment) = split_off_query_fragment(r);
185+
let (path, query, fragment) = split_off_query_fragment(&r);
184186

185187
return Ok(Request::Relative {
186188
path,
187189
force_in_lookup_dir: false,
188-
query: query.to_resolved().await?,
189-
fragment: fragment.to_resolved().await?,
190+
query: ResolvedVc::cell(query),
191+
fragment: ResolvedVc::cell(fragment),
190192
});
191193
}
192194

193195
if WINDOWS_PATH.is_match(&r) {
194-
let (path, query, fragment) = split_off_query_fragment(r);
196+
let (path, query, fragment) = split_off_query_fragment(&r);
195197

196198
return Ok(Request::Windows {
197199
path,
198-
query: query.to_resolved().await?,
199-
fragment: fragment.to_resolved().await?,
200+
query: ResolvedVc::cell(query),
201+
fragment: ResolvedVc::cell(fragment),
200202
});
201203
}
202204

@@ -227,13 +229,13 @@ impl Request {
227229
.captures(&r)
228230
.and_then(|caps| caps.get(1).zip(caps.get(2)))
229231
{
230-
let (path, query, fragment) = split_off_query_fragment(path.as_str().into());
232+
let (path, query, fragment) = split_off_query_fragment(path.as_str());
231233

232234
return Ok(Request::Module {
233235
module: module.as_str().into(),
234236
path,
235-
query: query.to_resolved().await?,
236-
fragment: fragment.to_resolved().await?,
237+
query: ResolvedVc::cell(query),
238+
fragment: ResolvedVc::cell(fragment),
237239
});
238240
}
239241

@@ -817,3 +819,63 @@ pub async fn stringify_data_uri(
817819
data.await?
818820
))
819821
}
822+
823+
#[cfg(test)]
824+
mod tests {
825+
use super::*;
826+
827+
#[test]
828+
fn test_split_query_fragment() {
829+
assert_eq!(
830+
(
831+
Pattern::Constant("foo".into()),
832+
RcStr::default(),
833+
RcStr::default()
834+
),
835+
split_off_query_fragment("foo")
836+
);
837+
// These two cases are a bit odd, but it is important to treat `import './foo?'` differently
838+
// from `import './foo'`, ditto for fragments.
839+
assert_eq!(
840+
(
841+
Pattern::Constant("foo".into()),
842+
RcStr::from("?"),
843+
RcStr::default()
844+
),
845+
split_off_query_fragment("foo?")
846+
);
847+
assert_eq!(
848+
(
849+
Pattern::Constant("foo".into()),
850+
RcStr::default(),
851+
RcStr::from("#")
852+
),
853+
split_off_query_fragment("foo#")
854+
);
855+
assert_eq!(
856+
(
857+
Pattern::Constant("foo".into()),
858+
RcStr::from("?bar=baz"),
859+
RcStr::default()
860+
),
861+
split_off_query_fragment("foo?bar=baz")
862+
);
863+
assert_eq!(
864+
(
865+
Pattern::Constant("foo".into()),
866+
RcStr::default(),
867+
RcStr::from("#stuff?bar=baz")
868+
),
869+
split_off_query_fragment("foo#stuff?bar=baz")
870+
);
871+
872+
assert_eq!(
873+
(
874+
Pattern::Constant("foo".into()),
875+
RcStr::from("?bar=baz"),
876+
RcStr::from("#stuff")
877+
),
878+
split_off_query_fragment("foo?bar=baz#stuff")
879+
);
880+
}
881+
}

turbopack/crates/turbopack-css/src/chunk/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ impl CssChunk {
194194
ServerFileSystem::new().root().to_resolved().await?
195195
},
196196
query: ResolvedVc::cell(RcStr::default()),
197-
fragment: None,
197+
fragment: ResolvedVc::cell(RcStr::default()),
198198
assets,
199199
modifiers: Vec::new(),
200200
parts: Vec::new(),

turbopack/crates/turbopack-ecmascript/src/chunk/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl Chunk for EcmascriptChunk {
123123
ServerFileSystem::new().root().to_resolved().await?
124124
},
125125
query: ResolvedVc::cell(RcStr::default()),
126-
fragment: None,
126+
fragment: ResolvedVc::cell(RcStr::default()),
127127
assets,
128128
modifiers: Vec::new(),
129129
parts: Vec::new(),

0 commit comments

Comments
 (0)