feat: path sanitizing in core
CI / format (push) Has been cancelled
CI / test (push) Has been cancelled
CI / packaging (push) Has been cancelled

This commit is contained in:
Tien Do Nam
2026-08-01 17:17:41 +02:00
parent b13bbeb3cd
commit da4464def2
22 changed files with 588 additions and 48 deletions
+1
View File
@@ -2,3 +2,4 @@
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
/build/
@@ -0,0 +1,15 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:localsend_isolates/rust/frb_generated.dart';
/// Rewrites `name` into a file name that is legal on the current platform,
/// replacing illegal characters with `_`.
String sanitizeFileName({required String name}) => RustLib.instance.api.crateApiFilenameSanitizeFileName(name: name);
/// Whether `name` is a legal file name on the current platform, i.e. whether
/// [sanitize_file_name] would leave it untouched.
bool isValidFileName({required String name}) => RustLib.instance.api.crateApiFilenameIsValidFileName(name: name);
@@ -10,6 +10,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:localsend_isolates/rust/api/cancel.dart';
import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/discovery.dart';
import 'package:localsend_isolates/rust/api/filename.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
@@ -74,7 +75,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => -895476497;
int get rustContentHash => -1120530143;
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
stem: 'rust_lib_localsend_app',
@@ -271,6 +272,10 @@ abstract class RustLibApi extends BaseApi {
Stream<RsHashFileEvent> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken});
bool crateApiFilenameIsValidFileName({required String name});
String crateApiFilenameSanitizeFileName({required String name});
Future<RsDiscovery> crateApiDiscoveryStartDiscovery({
required String group,
required int port,
@@ -1895,6 +1900,56 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ['sink', 'path', 'fileDescriptor', 'bytes', 'cancelToken'],
);
@override
bool crateApiFilenameIsValidFileName({required String name}) {
return handler.executeSync(
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(name, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 52)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_bool,
decodeErrorData: null,
),
constMeta: kCrateApiFilenameIsValidFileNameConstMeta,
argValues: [name],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiFilenameIsValidFileNameConstMeta => const TaskConstMeta(
debugName: 'is_valid_file_name',
argNames: ['name'],
);
@override
String crateApiFilenameSanitizeFileName({required String name}) {
return handler.executeSync(
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(name, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 53)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
decodeErrorData: null,
),
constMeta: kCrateApiFilenameSanitizeFileNameConstMeta,
argValues: [name],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiFilenameSanitizeFileNameConstMeta => const TaskConstMeta(
debugName: 'sanitize_file_name',
argNames: ['name'],
);
@override
Future<RsDiscovery> crateApiDiscoveryStartDiscovery({
required String group,
@@ -1930,7 +1985,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_String(certPem, serializer);
sse_encode_String(privateKeyPem, serializer);
sse_encode_u_64(timeoutMs, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 52, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 54, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery,
@@ -2005,7 +2060,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_opt_String(pin, serializer);
sse_encode_opt_box_autoadd_web_send_params(webSend, serializer);
sse_encode_opt_String(showToken, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 53, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer,
@@ -2031,7 +2086,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(cert, serializer);
sse_encode_String(publicKey, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 54, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 56, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -11,6 +11,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
import 'package:localsend_isolates/rust/api/cancel.dart';
import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/discovery.dart';
import 'package:localsend_isolates/rust/api/filename.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
@@ -13,6 +13,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
import 'package:localsend_isolates/rust/api/cancel.dart';
import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/discovery.dart';
import 'package:localsend_isolates/rust/api/filename.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:gal/gal.dart';
import 'package:legalize/legalize.dart';
import 'package:localsend_isolates/rust/api/filename.dart' as rust_filename;
import 'package:localsend_isolates/util/android_channel.dart' as android_channel;
import 'package:localsend_isolates/util/content_uri_helper.dart';
import 'package:localsend_isolates/util/file_path_helper.dart';
@@ -183,7 +183,7 @@ Future<(String, String?, String)> digestFilePathAndPrepareDirectory({
return (destinationUri, documentUri, p.basename(fileName));
}
final actualFileName = legalizeFilename(p.basename(fileName), os: Platform.operatingSystem);
final actualFileName = rust_filename.sanitizeFileName(name: p.basename(fileName));
final fileNameParts = p.split(fileName);
final dir = p.joinAll([parentDirectory, ...fileNameParts.take(fileNameParts.length - 1)]);
-8
View File
@@ -344,14 +344,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
legalize:
dependency: "direct main"
description:
name: legalize
sha256: bc3068aa4f14588575c8b5ba2a9e608c242dad325e7f7c56fedd68adba33526a
url: "https://pub.dev"
source: hosted
version: "1.2.2"
lints:
dependency: transitive
description:
-1
View File
@@ -17,7 +17,6 @@ dependencies:
flutter_rust_bridge: 2.12.0
freezed_annotation: 3.1.0
gal: 2.3.2
legalize: 1.2.2
logging: 1.3.0
mime: 2.0.0
path: 1.9.1
@@ -0,0 +1,16 @@
use flutter_rust_bridge::frb;
use localsend::util::filename;
/// Rewrites `name` into a file name that is legal on the current platform,
/// replacing illegal characters with `_`.
#[frb(sync)]
pub fn sanitize_file_name(name: String) -> String {
filename::sanitize(&name, filename::Rules::current())
}
/// Whether `name` is a legal file name on the current platform, i.e. whether
/// [sanitize_file_name] would leave it untouched.
#[frb(sync)]
pub fn is_valid_file_name(name: String) -> bool {
filename::is_valid(&name, filename::Rules::current())
}
@@ -1,6 +1,7 @@
pub mod cancel;
pub mod crypto;
pub mod discovery;
pub mod filename;
pub mod http;
pub mod logging;
pub mod model;
@@ -44,7 +44,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -895476497;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1120530143;
// Section: executor
@@ -3042,6 +3042,68 @@ fn wire__crate__api__crypto__hash_file_impl(
},
)
}
fn wire__crate__api__filename__is_valid_file_name_impl(
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "is_valid_file_name",
port: None,
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_name = <String>::sse_decode(&mut deserializer);
deserializer.end();
transform_result_sse::<_, ()>((move || {
let output_ok =
Result::<_, ()>::Ok(crate::api::filename::is_valid_file_name(api_name))?;
Ok(output_ok)
})())
},
)
}
fn wire__crate__api__filename__sanitize_file_name_impl(
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "sanitize_file_name",
port: None,
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_name = <String>::sse_decode(&mut deserializer);
deserializer.end();
transform_result_sse::<_, ()>((move || {
let output_ok =
Result::<_, ()>::Ok(crate::api::filename::sanitize_file_name(api_name))?;
Ok(output_ok)
})())
},
)
}
fn wire__crate__api__discovery__start_discovery_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -5044,9 +5106,9 @@ fn pde_ffi_dispatcher_primary_impl(
data_len,
),
51 => wire__crate__api__crypto__hash_file_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__api__discovery__start_discovery_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__api__discovery__start_discovery_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
56 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@@ -5063,6 +5125,8 @@ fn pde_ffi_dispatcher_sync_impl(
6 => wire__crate__api__cancel__RsCancellationToken_cancel_impl(ptr, rust_vec_len, data_len),
45 => wire__crate__api__cancel__create_cancellation_token_impl(ptr, rust_vec_len, data_len),
46 => wire__crate__api__http__create_client_impl(ptr, rust_vec_len, data_len),
52 => wire__crate__api__filename__is_valid_file_name_impl(ptr, rust_vec_len, data_len),
53 => wire__crate__api__filename__sanitize_file_name_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@@ -1,12 +1,17 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:localsend_isolates/rust/frb_generated.dart';
import 'package:localsend_isolates/src/task/server/file_saver.dart';
import 'package:path/path.dart' as p;
void main() {
late Directory tempDir;
setUpAll(() {
RustLib.initMock(api: _MockRustLibApi());
});
setUp(() {
tempDir = Directory.systemTemp.createTempSync('file_saver_test');
});
@@ -57,3 +62,12 @@ void main() {
);
});
}
/// The sanitizer lives in the Rust library, which is not loaded in unit tests.
class _MockRustLibApi implements RustLibApi {
@override
String crateApiFilenameSanitizeFileName({required String name}) => name;
@override
dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError('Not mocked: ${invocation.memberName}');
}