From e02dc0057a834fb21e50e98776b5c3e97a07e4b1 Mon Sep 17 00:00:00 2001 From: Zihan Dai <99155080+PDGGK@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:47:47 +1000 Subject: [PATCH 1/2] fix(services/dbfs): root the put path and stop encoding paths in JSON bodies Two defects in how this service addresses paths, both in core.rs. The path in the dbfs/put body is not rooted: let req_body = &json!({ "path": path, Every sibling roots it first -- create_dir at :60, delete at :89, rename at :116, list at :142, get-status at :193 all call build_rooted_abs_path. The value arrives from DbfsBackend::write as the operator-relative path and nothing else applies the root, so with any non-default root a write and the stat, list or delete that follows it address different DBFS paths. And four values are percent-encoded inside JSON request bodies: "path": percent_encode_path(&p), // :65, :94 "source_path": percent_encode_path(&source), // :126 "destination_path": percent_encode_path(&target), // :127 Nothing URL-decodes a JSON string value. The genuine query strings at :149 and :200 are encoded, and the server does decode those -- so the two halves of this file disagree with each other. create_dir("my dir/") creates a directory literally named my%20dir, while the get-status that follows asks for "my dir" and gets NotFound. For delete it is worse than an error: deleter.rs notes the server answers 200 even when the path does not exist, so the call reports success having removed nothing. A repo-wide grep for percent_encode_path inside json! across core/services matches these four lines and nothing else; dbfs is the only service in the tree that does it. Neither change affects an ordinary path. percent_encode_path leaves A-Z a-z 0-9 / - _ . ! ~ * ' ( ) untouched, so a plain key serialises identically; only a path containing a space, %, #, ?, &, +, ,, :, =, @ or a non-ASCII byte changes, and those are the ones broken today. One test, on the only one of the four that is a synchronous request builder. The other three send inside async methods, so pinning them would mean restructuring; their evidence is the in-file inconsistency above. Reverting the rooting fails that test and nothing else. The lister emits root-prefixed entry paths as well -- that is a separate change to a separate file and is not in here. Note for rebasing: #7801 renames build_rooted_abs_path in this file. If it lands first the new call in dbfs_create_file_request needs the new name. --- core/services/dbfs/src/core.rs | 35 +++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/core/services/dbfs/src/core.rs b/core/services/dbfs/src/core.rs index 15c8d9ff9b22..04abe74feaa4 100644 --- a/core/services/dbfs/src/core.rs +++ b/core/services/dbfs/src/core.rs @@ -62,7 +62,7 @@ impl DbfsCore { .to_string(); let req_body = &json!({ - "path": percent_encode_path(&p), + "path": p, }); let body = Buffer::from(Bytes::from(req_body.to_string())); @@ -91,7 +91,7 @@ impl DbfsCore { .to_string(); let request_body = &json!({ - "path": percent_encode_path(&p), + "path": p, // TODO: support recursive toggle, should we add a new field in OpDelete? "recursive": true, }); @@ -123,8 +123,8 @@ impl DbfsCore { req = req.header(header::AUTHORIZATION, auth_header_content); let req_body = &json!({ - "source_path": percent_encode_path(&source), - "destination_path": percent_encode_path(&target), + "source_path": source, + "destination_path": target, }); let body = Buffer::from(Bytes::from(req_body.to_string())); @@ -172,7 +172,7 @@ impl DbfsCore { req = req.header(header::AUTHORIZATION, auth_header_content); let req_body = &json!({ - "path": path, + "path": build_rooted_abs_path(&self.root, path), "contents": contents, "overwrite": true, }); @@ -290,3 +290,28 @@ mod error { } pub(super) use error::*; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_put_body_carries_the_rooted_unencoded_path() { + let core = DbfsCore { + root: "/data/".to_string(), + endpoint: "https://example.cloud.databricks.com".to_string(), + token: "token".to_string(), + }; + + let req = core + .dbfs_create_file_request("my file.txt", Bytes::from_static(b"hello")) + .expect("request must build"); + + let body: serde_json::Value = + serde_json::from_slice(&req.into_body().to_bytes()).expect("body must parse"); + + // Rooted, so a write lands where stat and list look for it; and not percent-encoded, + // because nothing decodes a JSON string value. + assert_eq!(body["path"], "/data/my file.txt"); + } +} From 6eac96bd8d3f275567feeccc0d987ba19ce7a5db Mon Sep 17 00:00:00 2001 From: Zihan Dai <99155080+PDGGK@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:19:41 +1000 Subject: [PATCH 2/2] Remove the test module --- core/services/dbfs/src/core.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/core/services/dbfs/src/core.rs b/core/services/dbfs/src/core.rs index 04abe74feaa4..af74079d017e 100644 --- a/core/services/dbfs/src/core.rs +++ b/core/services/dbfs/src/core.rs @@ -290,28 +290,3 @@ mod error { } pub(super) use error::*; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn the_put_body_carries_the_rooted_unencoded_path() { - let core = DbfsCore { - root: "/data/".to_string(), - endpoint: "https://example.cloud.databricks.com".to_string(), - token: "token".to_string(), - }; - - let req = core - .dbfs_create_file_request("my file.txt", Bytes::from_static(b"hello")) - .expect("request must build"); - - let body: serde_json::Value = - serde_json::from_slice(&req.into_body().to_bytes()).expect("body must parse"); - - // Rooted, so a write lands where stat and list look for it; and not percent-encoded, - // because nothing decodes a JSON string value. - assert_eq!(body["path"], "/data/my file.txt"); - } -}