Files
rustdesk/flutter/lib/utils/download_paths.dart
T
fufesou b44b71cfab Fix/download path validation (#16601)
* fix: validate download destinations and peer filename rules

Reject unsafe or linked download destinations and validate empty-directory
paths before creating them. Bind overwrite approval to accepted transfer
entries and propagate receiver errors to the UI.

Validate remote listings using the peer's filename rules so Unix names
remain browsable on Windows. Reject locally unsafe basenames before
starting downloads from either the Flutter or legacy UI.

Finish the previous file before validating the next digest so rejecting
the next destination preserves the file already received.

Require confirmation before restarting interrupted downloads with stale
staging files. Preserve matching resumes and cover receiver recovery.

Retain explicit overwrite rejection for peers older than 1.1.10 that
cannot negotiate confirmation.

Signed-off-by: fufesou <linlong1266@gmail.com>

* comments

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: use async I/O for download preparation and resume

Use Tokio filesystem APIs for download setup and resume metadata checks. Run the Windows link-count query on the blocking pool and flush digest writes before returning.

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-10-21 11:55:00 +08:00

53 lines
1.9 KiB
Dart

import 'dart:io';
import 'package:path/path.dart' as path;
class DownloadPaths {
static Future<void> ensureNoLinks(String root, String destination) async {
if (!path.isWithin(root, destination)) {
throw FormatException('Invalid download directory: $destination');
}
var current = root;
for (final name in path.split(path.relative(destination, from: root))) {
current = path.join(current, name);
if (await FileSystemEntity.type(current, followLinks: false) ==
FileSystemEntityType.link) {
throw FormatException('Download directory contains a link: $current');
}
}
}
static bool validName(String name, {required bool windows}) {
if (name.isEmpty || name == '.' || name == '..' || name.contains('\x00')) {
return false;
}
if (name.contains('/')) return false;
return !windows ||
(!name.contains(RegExp(r'[<>:"\\|?*]')) &&
!name.endsWith('.') &&
!name.endsWith(' '));
}
static String mapDirectory(
String remoteRoot, String remotePath, String localRoot,
{required bool remoteWindows, required bool localWindows}) {
final remote = path.Context(
style: remoteWindows ? path.Style.windows : path.Style.posix);
final local = path.Context(
style: localWindows ? path.Style.windows : path.Style.posix);
if (remotePath.contains('\x00') ||
remote.split(remotePath).contains('..') ||
(!remote.equals(remoteRoot, remotePath) &&
!remote.isWithin(remoteRoot, remotePath))) {
throw FormatException('Invalid download directory: $remotePath');
}
final relative = remote.relative(remotePath, from: remoteRoot);
if (relative == '.') return localRoot;
final names = remote.split(relative);
if (names.any((name) => !validName(name, windows: localWindows))) {
throw FormatException('Invalid download directory: $remotePath');
}
return local.joinAll([localRoot, ...names]);
}
}