Skip to content

Commit 43ccc01

Browse files
committed
[fix](tvf) Align guarded routes with filesystem consumers
### What problem does this PR solve? Issue Number: N/A Related PR: #69527 Problem Summary: Align glob discovery routes and exact nested ViewFS identity, and preserve supported remote providers. Preserve local Lance alias semantics with SDK-owned URI parsing and make paired Rust repair retryable. Keep the existing LOCAL setting, privileges and containment requirements. ### Release note Guarded Hadoop requests use exact nested ViewFS identities and reject escaped local listing routes. Supported provider compatibility is preserved. ### Check List (For Author) - Test: Unit Test (107 FE, 28 BE, SDK and build controls), isolated ViewFS endpoint comparison - Behavior changed: Yes (guarded route enforcement and supported provider compatibility) - Does this need documentation: Yes (startup and upgrade guidance remains pending)
1 parent d8dfb58 commit 43ccc01

9 files changed

Lines changed: 605 additions & 30 deletions

File tree

‎be/cmake/thirdparty.cmake‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,13 @@ add_thirdparty(arrow_flight_sql LIB64)
114114
add_thirdparty(arrow_dataset LIB64)
115115
add_thirdparty(arrow_acero LIB64)
116116
add_thirdparty(parquet LIB64)
117-
# Never consume a header/archive pair left by an interrupted Lance installation.
118-
if (EXISTS "${THIRDPARTY_DIR}/lib64/.lance-installing")
117+
# Never consume Rust SDK artifacts left by individual or paired publication failure.
118+
if (EXISTS "${THIRDPARTY_DIR}/lib64/.lance-installing" OR
119+
EXISTS "${THIRDPARTY_DIR}/lib64/.paimon-installing" OR
120+
EXISTS "${THIRDPARTY_DIR}/lib64/.lance-paimon-repairing")
119121
message(FATAL_ERROR
120-
"Lance SDK installation is incomplete. Retry thirdparty/build-thirdparty.sh lance_c "
121-
"or refresh include/lance and lib64/liblance_c.a as a matching pair.")
122+
"Rust SDK installation is incomplete. Retry the paired repair with build.sh --be "
123+
"or refresh the matching Lance/Paimon headers and archives.")
122124
endif ()
123125
add_thirdparty(lance_c LIB64 NOTADD)
124126
# liblance_c.a contains compiler_builtins cbrt symbols. Place libm before it

‎be/src/format_v2/table/lance_reader.cpp‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
#ifndef LANCE_STORAGE_BASES_JSON_VERSION
5050
#error "Rebuild lance-c with TVF storage-base metadata support before building Doris"
5151
#endif
52+
#ifndef LANCE_LOCAL_URI_PATH_JSON_VERSION
53+
#error "Rebuild lance-c with pre-open local URI parsing support before building Doris"
54+
#endif
5255

5356
namespace doris::format::lance {
5457

@@ -140,6 +143,49 @@ bool is_local_lance_tvf(const TFileRangeDesc& range, const TFileScanRangeParams&
140143
return local && (params.version == 0 || (params.__isset.is_tvf && params.is_tvf));
141144
}
142145

146+
std::string local_provider_uri(const std::string& scheme, const std::string& path) {
147+
// This is a URI path, not form encoding: '+' and '%' are literal filename bytes,
148+
// while '/' separates canonical path components. Preserve the selected SDK provider.
149+
constexpr char hex[] = "0123456789ABCDEF";
150+
std::string uri = scheme + "://";
151+
for (unsigned char value : path) {
152+
if ((value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') ||
153+
(value >= '0' && value <= '9') || value == '-' || value == '.' || value == '_' ||
154+
value == '~' || value == '/') {
155+
uri.push_back(static_cast<char>(value));
156+
} else {
157+
uri.push_back('%');
158+
uri.push_back(hex[value >> 4]);
159+
uri.push_back(hex[value & 15]);
160+
}
161+
}
162+
return uri;
163+
}
164+
165+
Status local_uri_path(const std::string& requested, std::string* scheme, std::string* path) {
166+
// Use the pinned SDK's default registry before any manifest/store open. The aliases
167+
// are non-special URLs; changing them to file: before parsing changes their meaning.
168+
std::unique_ptr<const char, decltype(&lance_free_string)> json(
169+
lance_local_uri_path_json(requested.c_str()), &lance_free_string);
170+
if (!json) {
171+
return Status::InvalidArgument("local Lance dataset path is not allowed");
172+
}
173+
rapidjson::Document metadata;
174+
metadata.Parse(json.get());
175+
if (metadata.HasParseError() || !metadata.IsObject() || !metadata.HasMember("format_version") ||
176+
!metadata["format_version"].IsInt() || metadata["format_version"].GetInt() != 1 ||
177+
!metadata.HasMember("scheme") || !metadata["scheme"].IsString() ||
178+
!metadata.HasMember("local_path") || !metadata["local_path"].IsString()) {
179+
return Status::InvalidArgument("Invalid Lance local URI metadata");
180+
}
181+
scheme->assign(metadata["scheme"].GetString(), metadata["scheme"].GetStringLength());
182+
path->assign(metadata["local_path"].GetString(), metadata["local_path"].GetStringLength());
183+
if (*scheme != "file-object-store" && *scheme != "file+uring") {
184+
return Status::InvalidArgument("LOCAL Lance TVF requires a local provider");
185+
}
186+
return Status::OK();
187+
}
188+
143189
Status resolve_dataset_uri(const TFileRangeDesc& range, const TFileScanRangeParams& scan_params,
144190
std::string* uri) {
145191
const auto& requested_uri = range.table_format_params.lance_params.dataset_uri;
@@ -148,6 +194,9 @@ Status resolve_dataset_uri(const TFileRangeDesc& range, const TFileScanRangePara
148194
if (is_local_lance_tvf(range, scan_params)) {
149195
// LOCAL Lance bypasses glob because it opens a dataset directory. Apply the same
150196
// canonical secure-root check before either schema discovery or a cached dataset open.
197+
if (requested_uri.empty() || requested_uri.find('\0') != std::string::npos) {
198+
return Status::InvalidArgument("local Lance dataset path is not allowed");
199+
}
151200
std::string local_input = requested_uri;
152201
const auto colon = requested_uri.find(':');
153202
const auto slash = requested_uri.find('/');
@@ -164,9 +213,20 @@ Status resolve_dataset_uri(const TFileRangeDesc& range, const TFileScanRangePara
164213
.lexically_normal()
165214
.string();
166215
}
216+
std::string scheme;
217+
io::Path parsed;
218+
if (!bare_path && !io::LocalFileSystem::parse_local_path(requested_uri, parsed).ok()) {
219+
// Keep the established file: URL normalization (including encoded slashes
220+
// beside symlinks). Only unsupported local aliases need the SDK path parser.
221+
RETURN_IF_ERROR(local_uri_path(requested_uri, &scheme, &local_input));
222+
}
167223
RETURN_IF_ERROR(io::global_local_filesystem()->canonicalize_local_file(
168224
config::user_files_secure_path, local_input, uri));
169-
return validate_local_dataset_files(*uri);
225+
RETURN_IF_ERROR(validate_local_dataset_files(*uri));
226+
if (!scheme.empty()) {
227+
*uri = local_provider_uri(scheme, *uri);
228+
}
229+
return Status::OK();
170230
}
171231
*uri = requested_uri;
172232
return Status::OK();

‎be/test/format_v2/table/lance_reader_test.cpp‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4317,6 +4317,93 @@ TEST(LanceTableReaderTypeTest, ReadsTemporalTypesFromAllTypesFixture) {
43174317
} // namespace doris::format::lance
43184318

43194319
namespace doris::format::lance {
4320+
void assert_local_alias_schema_and_rows(const std::string& requested, const std::string& expected) {
4321+
TQueryGlobals globals;
4322+
RuntimeState state(globals);
4323+
RuntimeProfile profile("lance_local_alias");
4324+
TFileScanRangeParams scan_params;
4325+
LanceTableReader reader;
4326+
const Columns columns = {projected_column("row_id", TYPE_BIGINT, false)};
4327+
ASSERT_TRUE(init_reader(&reader, columns, &state, &profile, &scan_params).ok());
4328+
auto range = make_latest_lance_range(requested);
4329+
range.__set_file_type(TFileType::FILE_LOCAL);
4330+
std::vector<std::string> names;
4331+
std::vector<DataTypePtr> types;
4332+
ASSERT_TRUE(reader.fetch_schema(range, scan_params, &names, &types).ok()) << requested;
4333+
EXPECT_FALSE(names.empty());
4334+
LanceTableReader::DatasetKey key;
4335+
ASSERT_TRUE(reader._dataset_key(range, &key).ok());
4336+
EXPECT_EQ(expected, key.uri);
4337+
ASSERT_TRUE(prepare_range(&reader, range).ok()) << requested;
4338+
Block block;
4339+
add_output_columns(&block, columns);
4340+
std::vector<int64_t> rows;
4341+
bool eos = false;
4342+
while (!eos) {
4343+
const auto read_status = reader.get_block(&block, &eos);
4344+
ASSERT_TRUE(read_status.ok()) << requested << ": " << read_status.to_string();
4345+
if (!eos) {
4346+
const auto& values =
4347+
assert_cast<const ColumnInt64&>(*block.get_by_position(0).column).get_data();
4348+
rows.insert(rows.end(), values.begin(), values.end());
4349+
block.clear_column_data();
4350+
}
4351+
}
4352+
std::ranges::sort(rows);
4353+
EXPECT_EQ((std::vector<int64_t> {1, 2, 3, 4}), rows);
4354+
ASSERT_TRUE(reader.close().ok());
4355+
}
4356+
4357+
TEST(LanceTableReaderLocalPathTest, LocalAliasesPreserveProviderForSchemaAndScan) {
4358+
const auto original_secure_path = config::user_files_secure_path;
4359+
const auto owned =
4360+
std::filesystem::temp_directory_path() /
4361+
("lance_local_alias_" +
4362+
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
4363+
const auto root = owned / "secure";
4364+
const auto outside = owned / "outside";
4365+
DEFER({
4366+
config::user_files_secure_path = original_secure_path;
4367+
std::error_code error;
4368+
std::filesystem::remove_all(owned, error);
4369+
});
4370+
std::filesystem::create_directories(root / "a b%+.lance");
4371+
std::filesystem::create_directories(outside);
4372+
const auto fixture =
4373+
std::filesystem::canonical("./be/test/format_v2/table/lance/data/all_types.lance");
4374+
std::filesystem::copy(fixture, root / "a b%+.lance", std::filesystem::copy_options::recursive);
4375+
std::filesystem::create_directory_symlink(root / "a b%+.lance", root / "in-root");
4376+
std::filesystem::create_directory_symlink(outside, root / "escape");
4377+
config::user_files_secure_path = root.string();
4378+
std::vector<std::string> schemes {"file-object-store"};
4379+
#ifdef __linux__
4380+
// io_uring setup can be prohibited by the host; explicitly select this extra provider.
4381+
const auto* enable_uring = std::getenv("DORIS_LANCE_TEST_IO_URING");
4382+
if (enable_uring != nullptr && std::strcmp(enable_uring, "1") == 0) {
4383+
schemes.emplace_back("file+uring");
4384+
}
4385+
#endif
4386+
for (const auto& scheme : schemes) {
4387+
const auto expected = scheme + "://" + (root / "a%20b%25%2B.lance").string();
4388+
for (const auto& requested :
4389+
{expected + "?ignored=%00#tag", scheme + "://host" + (root / "in-root").string()}) {
4390+
ASSERT_NO_FATAL_FAILURE(assert_local_alias_schema_and_rows(requested, expected));
4391+
}
4392+
TFileScanRangeParams scan_params;
4393+
LanceTableReader reader;
4394+
reader._scan_params = &scan_params;
4395+
for (const auto& denied : {outside, root / "escape"}) {
4396+
auto range = make_latest_lance_range(scheme + "://" + denied.string());
4397+
range.__set_file_type(TFileType::FILE_LOCAL);
4398+
std::vector<std::string> names;
4399+
std::vector<DataTypePtr> types;
4400+
LanceTableReader::DatasetKey key;
4401+
EXPECT_FALSE(reader.fetch_schema(range, scan_params, &names, &types).ok());
4402+
EXPECT_FALSE(reader._dataset_key(range, &key).ok());
4403+
}
4404+
}
4405+
}
4406+
43204407
TEST(LanceTableReaderLocalPathTest, SchemaAndScanUseTheSameSecurePath) {
43214408
const auto original_secure_path = config::user_files_secure_path;
43224409
DEFER(config::user_files_secure_path = original_secure_path);

‎build.sh‎

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -451,12 +451,13 @@ fi
451451
# dependency set so Lance and Paimon also use the same selected Rust toolchain.
452452
# An interrupted header/archive publication must be repaired before either file is reused.
453453
if [[ ! -f "${DORIS_THIRDPARTY}/installed/lib/${LAST_THIRDPARTY_LIB}" ||
454-
! -f "${DORIS_THIRDPARTY}/installed/lib64/liblance_c.a" ||
455-
! -f "${DORIS_THIRDPARTY}/installed/lib64/libpaimon_c.a" ||
456-
! -f "${DORIS_THIRDPARTY}/installed/include/paimon_rust/paimon.h" ||
457-
! -s "${DORIS_THIRDPARTY}/installed/lib64/libpaimon_c.a" ||
458-
! -s "${DORIS_THIRDPARTY}/installed/include/paimon_rust/paimon.h" ||
459-
-e "${DORIS_THIRDPARTY}/installed/lib64/.paimon-installing" ]]; then
454+
( ! -e "${DORIS_THIRDPARTY}/installed/lib64/.lance-paimon-repairing" &&
455+
( ! -f "${DORIS_THIRDPARTY}/installed/lib64/liblance_c.a" ||
456+
! -f "${DORIS_THIRDPARTY}/installed/lib64/libpaimon_c.a" ||
457+
! -f "${DORIS_THIRDPARTY}/installed/include/paimon_rust/paimon.h" ||
458+
! -s "${DORIS_THIRDPARTY}/installed/lib64/libpaimon_c.a" ||
459+
! -s "${DORIS_THIRDPARTY}/installed/include/paimon_rust/paimon.h" ||
460+
-e "${DORIS_THIRDPARTY}/installed/lib64/.paimon-installing" ) ) ]]; then
460461
# Compilation images may contain only installed artifacts; never erase them without a rebuild source.
461462
if [[ ! -f "${DORIS_THIRDPARTY}/build-thirdparty.sh" ]]; then
462463
echo "Third-party dependencies require a rebuild, but build-thirdparty.sh is missing." >&2
@@ -483,11 +484,14 @@ lance_c_is_compatible() {
483484
-s "${install_dir}/lib64/liblance_c.a" ]] || return 1
484485
grep -Eq '^#[[:space:]]*define[[:space:]]+LANCE_STORAGE_BASES_JSON_VERSION[[:space:]]+1([[:space:]]|$)' \
485486
"${install_dir}/include/lance/lance.h" || return 1
487+
grep -Eq '^#[[:space:]]*define[[:space:]]+LANCE_LOCAL_URI_PATH_JSON_VERSION[[:space:]]+1([[:space:]]|$)' \
488+
"${install_dir}/include/lance/lance.h" || return 1
486489
# GNU nm and Darwin nm spell C exports differently. Read the whole stream:
487490
# early awk exit would turn a large archive's producer SIGPIPE into failure.
488491
"${NM:-nm}" -g "${install_dir}/lib64/liblance_c.a" 2>/dev/null | awk '
489-
NF >= 2 && $(NF - 1) ~ /^[TW]$/ && $NF ~ /^_?lance_dataset_storage_bases_json$/ { found = 1 }
490-
END { exit !found }
492+
NF >= 2 && $(NF - 1) ~ /^[TW]$/ && $NF ~ /^_?lance_dataset_storage_bases_json$/ { bases = 1 }
493+
NF >= 2 && $(NF - 1) ~ /^[TW]$/ && $NF ~ /^_?lance_local_uri_path_json$/ { local_uri = 1 }
494+
END { exit !(bases && local_uri) }
491495
'
492496
}
493497

@@ -505,9 +509,12 @@ lance_c_source_matches() {
505509
<(grep -E '^(LANCE_C|PAIMON_RUST)_(DOWNLOAD|NAME|SOURCE)=' "${source_dir}/vars.sh")
506510
}
507511

508-
if [[ "${BUILD_BE}" -eq 1 ]] && ! lance_c_is_compatible "${DORIS_THIRDPARTY}/installed"; then
512+
if [[ "${BUILD_BE}" -eq 1 ]] && {
513+
[[ -e "${DORIS_THIRDPARTY}/installed/lib64/.lance-paimon-repairing" ]] ||
514+
! lance_c_is_compatible "${DORIS_THIRDPARTY}/installed"
515+
}; then
509516
if ! lance_c_source_matches "${DORIS_THIRDPARTY}"; then
510-
echo "Lance SDK header/archive pair lacks LANCE_STORAGE_BASES_JSON_VERSION=1 or its exported API, or is incomplete." >&2
517+
echo "Lance SDK header/archive pair lacks the TVF storage-base/local-URI APIs, or is incomplete." >&2
511518
echo "Refresh the compilation image's include/lance and lib64/liblance_c.a together, or set DORIS_THIRDPARTY to matching complete third-party sources." >&2
512519
exit 1
513520
fi
@@ -519,12 +526,24 @@ if [[ "${BUILD_BE}" -eq 1 ]] && ! lance_c_is_compatible "${DORIS_THIRDPARTY}/ins
519526
fi
520527
paired_cargo="${LANCE_C_CARGO:-${PAIMON_RUST_CARGO:-${CARGO:-cargo}}}"
521528
echo "Rebuilding lance_c and paimon_rust together for the TVF storage-base metadata ABI ..."
529+
# Keep pair state across the gap after Lance publishes but before Paimon's installer.
530+
# A retry must repair both archives even when the new Lance ABI already looks complete.
531+
pair_marker="${DORIS_THIRDPARTY}/installed/lib64/.lance-paimon-repairing"
532+
mkdir -p "${DORIS_THIRDPARTY}/installed/lib64"
533+
touch "${pair_marker}"
522534
LANCE_C_CARGO="${paired_cargo}" PAIMON_RUST_CARGO="${paired_cargo}" \
523535
bash "${DORIS_THIRDPARTY}/build-thirdparty.sh" -j "${PARALLEL}" lance_c paimon_rust
524536
if ! lance_c_is_compatible "${DORIS_THIRDPARTY}/installed"; then
525537
echo "Rebuilt Lance SDK header/archive pair is incompatible or incomplete; refresh the matching pair before building BE." >&2
526538
exit 1
527539
fi
540+
if [[ -e "${DORIS_THIRDPARTY}/installed/lib64/.paimon-installing" ||
541+
! -s "${DORIS_THIRDPARTY}/installed/include/paimon_rust/paimon.h" ||
542+
! -s "${DORIS_THIRDPARTY}/installed/lib64/libpaimon_c.a" ]]; then
543+
echo "Rebuilt Paimon SDK header/archive pair is incomplete; retry the paired repair before building BE." >&2
544+
exit 1
545+
fi
546+
rm -f "${pair_marker}"
528547
fi
529548

530549
update_submodule() {

‎fe/be-java-extensions/hadoop-deps/src/main/java/org/apache/hadoop/fs/FileSystem.java‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3683,6 +3683,36 @@ public static String encode(List<Rule> rules) {
36833683
}
36843684
}
36853685

3686+
/** Routes touched by Globber, without constructing a filesystem or listing data. */
3687+
public static List<Rule> globRules(URI filesystem, String pattern) throws IOException {
3688+
List<Rule> rules = new ArrayList<>();
3689+
rules.add(new Rule(filesystem.toString(), pattern, true));
3690+
if (pattern.indexOf('\\') < 0) {
3691+
return rules;
3692+
}
3693+
for (String expanded : GlobExpander.expand(pattern)) {
3694+
Path flattened = new Path(expanded.isEmpty() ? Path.CUR_DIR : expanded);
3695+
String path = flattened.toUri().getPath();
3696+
Path candidate = new Path(flattened.isAbsolute() ? "/" : Path.CUR_DIR);
3697+
for (String component : path.split("/")) {
3698+
if (component.isEmpty()) {
3699+
continue;
3700+
}
3701+
GlobFilter filter = new GlobFilter(component);
3702+
if (filter.hasPattern()) {
3703+
// Globber lists this parent before it knows the matching child names. Keep
3704+
// prior wildcard components as a conservative subtree route, including when
3705+
// a later escaped '..' moves the final candidate outside that subtree.
3706+
rules.add(new Rule(filesystem.toString(), new Path(candidate, "*").toUri().getPath(), true));
3707+
}
3708+
component = component.replaceAll("\\\\(.)", "$1");
3709+
candidate = new Path(candidate, component);
3710+
}
3711+
rules.add(new Rule(filesystem.toString(), candidate.toUri().getPath(), true));
3712+
}
3713+
return rules;
3714+
}
3715+
36863716
private static IOException rejected() {
36873717
return new IOException("Hadoop TVF requires a proven remote filesystem route. "
36883718
+ "Use local() with LOCAL TVF privileges and secure-path restrictions.");
@@ -3778,6 +3808,10 @@ private FrozenConfiguration(Configuration source) throws IOException {
37783808
values.put(key, value);
37793809
}
37803810
}
3811+
// ViewFS's private InnerCache folds authority case (A/a). Guarded children must
3812+
// instead use FileSystem's exact-authority/profile cache below.
3813+
set("fs.viewfs.enable.inner.cache", "false");
3814+
values.put("fs.viewfs.enable.inner.cache", "false");
37813815
try {
37823816
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
37833817
java.io.DataOutputStream output = new java.io.DataOutputStream(
@@ -3922,11 +3956,6 @@ protected Provider provider(String scheme) {
39223956
}
39233957
try {
39243958
String implementation = FileSystem.getFileSystemClass(scheme, conf).getName();
3925-
if (Arrays.asList("io.juicefs.JuiceFileSystem", "org.apache.hadoop.fs.ozone.OzoneFileSystem",
3926-
"org.apache.hadoop.fs.ozone.RootedOzoneFileSystem", "org.apache.hadoop.fs.cosn.CosNFileSystem",
3927-
"com.qcloud.chdfs.fs.CHDFSHadoopFileSystemAdapter").contains(implementation)) {
3928-
return Provider.REMOTE;
3929-
}
39303959
return implementationProvider(implementation);
39313960
} catch (IOException | RuntimeException e) {
39323961
return Provider.UNKNOWN;
@@ -3951,6 +3980,12 @@ protected enum Provider {
39513980
"org.apache.hadoop.fs.azurebfs.AzureBlobFileSystem",
39523981
"org.apache.hadoop.fs.azurebfs.SecureAzureBlobFileSystem",
39533982
"org.apache.hadoop.fs.azure.NativeAzureFileSystem",
3983+
"org.apache.hadoop.fs.azure.NativeAzureFileSystem$Secure",
3984+
"io.juicefs.JuiceFileSystem",
3985+
"org.apache.hadoop.fs.ozone.OzoneFileSystem",
3986+
"org.apache.hadoop.fs.ozone.RootedOzoneFileSystem",
3987+
"org.apache.hadoop.fs.cosn.CosNFileSystem",
3988+
"com.qcloud.chdfs.fs.CHDFSHadoopFileSystemAdapter",
39543989
"org.apache.hadoop.fs.aliyun.oss.AliyunOSSFileSystem",
39553990
"com.aliyun.jindodata.oss.JindoOssFileSystem",
39563991
"org.apache.hadoop.fs.obs.OBSFileSystem",
@@ -4004,6 +4039,13 @@ public void checkRoute(URI filesystem, String path, Set<String> activeTables) th
40044039
checkViewFsEnabled(filesystem, path, activeTables);
40054040
}
40064041

4042+
public void checkGlobRoute(URI filesystem, String pattern, Set<String> activeTables)
4043+
throws IOException, URISyntaxException {
4044+
for (Rule rule : globRules(filesystem, pattern)) {
4045+
checkRoute(filesystem, rule.path, activeTables);
4046+
}
4047+
}
4048+
40074049
protected Provider provider(String scheme) {
40084050
if ("file".equalsIgnoreCase(scheme)) {
40094051
return Provider.LOCAL;

0 commit comments

Comments
 (0)