Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J SM Mobile
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / SM Mobile
175 Dateien
sm-mobile-20261016T185235Z-8e93ecc1b59a
Hinzugefügt: · Erstellt:
1
import 'dart:async';2
import 'dart:io';3
import 'dart:typed_data';4
import 'package:cryptography/cryptography.dart';5
import 'package:dartssh2/dartssh2.dart';6
import '../core/failure.dart';7
import '../models/server_profile.dart';9
String safeName(String name) {10
if (name.isEmpty ||11
name == '.' ||12
name == '..' ||13
name.length > 255 ||14
RegExp(r'[/\\\x00-\x1f\x7f]').hasMatch(name)) {15
throw const Failure('unsafePath');16
}17
return name;18
}20
String safePath(String path) {21
if (!path.startsWith('/') ||22
RegExp(r'[\\\x00-\x1f\x7f]').hasMatch(path) ||23
path.split('/').any((s) => s == '.' || s == '..')) {24
throw const Failure('unsafePath');25
}26
return path == '/' ? '/' : path.replaceAll(RegExp(r'/+$'), '');27
}29
String childPath(String parent, String name) =>30
'${safePath(parent) == '/' ? '' : safePath(parent)}/${safeName(name)}';31
String parentPath(String path) {32
safePath(path);33
final index = path.lastIndexOf('/');34
return index <= 0 ? '/' : path.substring(0, index);35
}37
class TransferToken {38
bool cancelled = false;39
void Function()? abort;40
void cancel() {41
cancelled = true;42
abort?.call();43
}45
void check() {46
if (cancelled) throw const Failure('cancelled');47
}48
}50
class SftpService {51
final SftpClient client;52
SftpService(this.client);53
Future<String> home() async => safePath(await client.absolute('.'));54
Future<void> guard(String path, {bool missingLeaf = false}) async {55
safePath(path);56
var current = '';57
final parts = path.split('/').where((p) => p.isNotEmpty).toList();58
for (var i = 0; i < parts.length; i++) {59
current += '/${safeName(parts[i])}';60
try {61
final attrs = await client.stat(current, followLink: false);62
if (attrs.isSymbolicLink ||63
(i < parts.length - 1 && !attrs.isDirectory)) {64
throw const Failure('unsafePath');65
}66
} on SftpStatusError catch (e) {67
if (missingLeaf && i == parts.length - 1 && e.code == 2) return;68
rethrow;69
}70
}71
}73
Future<List<SftpName>> list(String path) async {74
await guard(path);75
final entries = await client.listdir(path);76
final filtered = entries77
.where((e) => e.filename != '.' && e.filename != '..')78
.toList();79
for (final entry in filtered) {80
safeName(entry.filename);81
}82
filtered.sort(83
(a, b) => a.attr.isDirectory == b.attr.isDirectory84
? a.filename.toLowerCase().compareTo(b.filename.toLowerCase())85
: a.attr.isDirectory86
? -187
: 1,88
);89
return filtered;90
}92
Future<void> mkdir(String path) async {93
await guard(path, missingLeaf: true);94
await client.mkdir(path);95
}97
Future<void> rename(String oldPath, String newPath) async {98
await guard(oldPath);99
await guard(newPath, missingLeaf: true);100
await _absent(newPath);101
await client.rename(oldPath, newPath);102
}104
Future<void> remove(String path) async {105
if (path == '/') throw const Failure('unsafePath');106
await guard(path);107
final attrs = await client.stat(path, followLink: false);108
if (attrs.isDirectory) {109
await client.rmdir(path);110
} else {111
await client.remove(path);112
}113
}115
Future<void> _absent(String path) async {116
try {117
await client.stat(path, followLink: false);118
} on SftpStatusError catch (e) {119
if (e.code == 2) return;120
rethrow;121
}122
throw const Failure('fileExists');123
}125
Future<void> upload(126
Stream<Uint8List> stream,127
int size,128
String remote,129
TransferToken token,130
void Function(int, int) progress,131
) async {132
await guard(remote, missingLeaf: true);133
await _absent(remote);134
token.check();135
final temp = childPath(parentPath(remote), '.sm-${newId()}.part');136
SftpFile? file;137
var created = false;138
try {139
file = await client.open(140
temp,141
mode:142
SftpFileOpenMode.write |143
SftpFileOpenMode.create |144
SftpFileOpenMode.exclusive,145
);146
created = true;147
final writer = file.write(148
stream,149
onProgress: (n) {150
progress(n, size);151
},152
);153
token.abort = () {154
unawaited(writer.abort().catchError((Object _) {}));155
};156
if (token.cancelled) token.abort!();157
await writer.done;158
token.check();159
await file.close();160
file = null;161
await guard(remote, missingLeaf: true);162
await _absent(remote);163
await client.rename(temp, remote);164
} catch (_) {165
try {166
await file?.close();167
if (created) await client.remove(temp);168
} catch (_) {169
/* Session may already be closed. */170
}171
rethrow;172
} finally {173
token.abort = null;174
}175
}177
Future<void> download(178
String remote,179
File local,180
TransferToken token,181
void Function(int, int) progress,182
) async {183
await guard(remote);184
token.check();185
final attrs = await client.stat(remote, followLink: false);186
if (!attrs.isFile) throw const Failure('unsafePath');187
final file = await client.open(remote);188
final sink = local.openWrite();189
token.abort = () {190
unawaited(file.close().catchError((Object _) {}));191
};192
try {193
await sink.addStream(194
file195
.read(196
onProgress: (n) {197
progress(n, attrs.size ?? 0);198
},199
)200
.map((c) {201
token.check();202
return c;203
}),204
);205
token.check();206
await sink.flush();207
await sink.close();208
} catch (_) {209
try {210
await sink.close();211
} catch (_) {212
/* Preserve the original transfer failure. */213
}214
if (await local.exists()) await local.delete();215
rethrow;216
} finally {217
token.abort = null;218
try {219
await sink.close();220
} catch (_) {221
/* Already handled above. */222
}223
try {224
await file.close();225
} catch (_) {226
/* Cancellation closes the handle. */227
}228
}229
}231
/// Reads a whole regular file of at most [maxBytes].232
Future<(Uint8List, SftpFileAttrs)> readFile(233
String path, {234
required int maxBytes,235
TransferToken? token,236
void Function(int, int)? progress,237
}) async {238
await guard(path);239
final attrs = await client.stat(path, followLink: false);240
if (!attrs.isFile) throw const Failure('unsafePath');241
if ((attrs.size ?? 0) > maxBytes) throw const Failure('fileTooLarge');242
final file = await client.open(path);243
token?.abort = () {244
unawaited(file.close().catchError((Object _) {}));245
};246
try {247
final bytes = BytesBuilder(copy: false);248
await for (final chunk in file.read(249
onProgress: (n) => progress?.call(n, attrs.size ?? 0),250
)) {251
token?.check();252
bytes.add(chunk);253
if (bytes.length > maxBytes) throw const Failure('fileTooLarge');254
}255
return (bytes.takeBytes(), attrs);256
} finally {257
if (token != null) token.abort = null;258
try {259
await file.close();260
} catch (_) {261
/* Closed by cancellation. */262
}263
}264
}266
/// The file's attributes without following a symbolic link; null when it267
/// does not exist.268
Future<SftpFileAttrs?> statOrNull(String path) async {269
await guard(path, missingLeaf: true);270
try {271
return await client.stat(path, followLink: false);272
} on SftpStatusError catch (e) {273
if (e.code == 2) return null;274
rethrow;275
}276
}278
static Future<List<int>> sha256Of(List<int> bytes) async =>279
(await Sha256().hash(bytes)).bytes;281
static bool _same(List<int> a, List<int> b) {282
if (a.length != b.length) return false;283
for (var i = 0; i < a.length; i++) {284
if (a[i] != b[i]) return false;285
}286
return true;287
}289
/// Files up to this size are compared by content before overwriting, so a290
/// change within the same second with the same size is still noticed.291
static const contentCheckLimit = 4 * 1024 * 1024;293
/// Throws 'remoteChanged' when the file is no longer what was loaded:294
/// different size, modification time or (for small files) content.295
Future<SftpFileAttrs> checkUnchanged(296
String path,297
SftpFileAttrs expected,298
List<int>? expectedHash,299
) async {300
final current = await statOrNull(path);301
if (current == null) throw const Failure('remoteDeleted');302
if (!current.isFile) throw const Failure('unsafePath');303
if (current.size != expected.size ||304
current.modifyTime != expected.modifyTime) {305
throw const Failure('remoteChanged');306
}307
if (expectedHash != null && (current.size ?? 0) <= contentCheckLimit) {308
final (bytes, _) = await readFile(path, maxBytes: contentCheckLimit);309
if (!_same(await sha256Of(bytes), expectedHash)) {310
throw const Failure('remoteChanged');311
}312
}313
return current;314
}316
/// Saves [bytes] to the existing file [path] without risking it:317
///318
/// 1. unless [force], verifies the file was not changed since [expected];319
/// 2. writes a temporary file next to it and gives it the original320
/// permission bits (and owner/group where the server allows it);321
/// 3. replaces the original by renaming (atomic with OpenSSH's322
/// posix-rename; otherwise via a backup that is restored on failure).323
///324
/// A failed save leaves the original untouched. When a temporary file325
/// cannot be created there ('atomicNotPossible') or the owner would326
/// change ('ownerWouldChange'), nothing is written; the caller may ask327
/// the user and retry with [inPlace], which overwrites the file directly328
/// (keeps owner and links, but is not atomic).329
Future<SftpFileAttrs> saveFile(330
String path,331
Uint8List bytes, {332
SftpFileAttrs? expected,333
List<int>? expectedHash,334
bool force = false,335
bool inPlace = false,336
}) async {337
await guard(path, missingLeaf: true);338
var current = await statOrNull(path);339
if (current == null) {340
if (expected != null && !force) throw const Failure('remoteDeleted');341
return _createNew(path, bytes);342
}343
if (!current.isFile) throw const Failure('unsafePath');344
if (expected != null && !force) {345
current = await checkUnchanged(path, expected, expectedHash);346
}347
// Replacing via rename needs only folder permission; the user's write348
// permission on the file itself is checked first, without changing it.349
try {350
final probe = await client.open(path, mode: SftpFileOpenMode.write);351
await probe.close();352
} on SftpStatusError catch (e) {353
if (e.code == 3) throw const Failure('permissionDenied');354
rethrow;355
}356
if (inPlace) {357
final file = await client.open(358
path,359
mode: SftpFileOpenMode.write | SftpFileOpenMode.truncate,360
);361
try {362
await file.writeBytes(bytes);363
} finally {364
await file.close();365
}366
return client.stat(path, followLink: false);367
}368
final name = path.substring(path.lastIndexOf('/') + 1);369
final stem = name.length > 200 ? name.substring(0, 200) : name;370
final temp = childPath(parentPath(path), '.$stem.sm-${newId()}.tmp');371
SftpFile? file;372
var created = false;373
try {374
try {375
file = await client.open(376
temp,377
mode:378
SftpFileOpenMode.write |379
SftpFileOpenMode.create |380
SftpFileOpenMode.exclusive,381
);382
} on SftpStatusError catch (e) {383
if (e.code == 3) throw const Failure('atomicNotPossible');384
rethrow;385
}386
created = true;387
await file.writeBytes(bytes);388
await file.close();389
file = null;390
final mode = current.mode?.value;391
if (mode != null) {392
await client.setStat(393
temp,394
SftpFileAttrs(mode: SftpFileMode.value(mode & 0xfff)),395
);396
}397
final made = await client.stat(temp, followLink: false);398
if (current.userID != null &&399
(made.userID != current.userID || made.groupID != current.groupID)) {400
try {401
await client.setStat(402
temp,403
SftpFileAttrs(userID: current.userID, groupID: current.groupID),404
);405
} catch (_) {406
throw const Failure('ownerWouldChange');407
}408
}409
// The file may have changed while the copy was written.410
if (expected != null && !force) {411
await checkUnchanged(path, current, null);412
}413
try {414
await client.rename(temp, path);415
} on SftpStatusError {416
// No posix-rename: move the original aside, then put the new file417
// in place; restore the original if that fails.418
final backup = childPath(parentPath(path), '.$stem.sm-${newId()}.bak');419
await client.rename(path, backup);420
try {421
await client.rename(temp, path);422
} catch (_) {423
await client.rename(backup, path);424
rethrow;425
}426
try {427
await client.remove(backup);428
} catch (_) {429
/* The new file is in place; a stray backup is harmless. */430
}431
}432
created = false;433
return await client.stat(path, followLink: false);434
} finally {435
if (file != null) {436
try {437
await file.close();438
} catch (_) {439
/* Session may be gone. */440
}441
}442
if (created) {443
try {444
await client.remove(temp);445
} catch (_) {446
/* Session may be gone; the original is untouched. */447
}448
}449
}450
}452
Future<SftpFileAttrs> _createNew(String path, Uint8List bytes) async {453
final file = await client.open(454
path,455
mode:456
SftpFileOpenMode.write |457
SftpFileOpenMode.create |458
SftpFileOpenMode.exclusive,459
);460
try {461
await file.writeBytes(bytes);462
} catch (_) {463
try {464
await file.close();465
await client.remove(path);466
} catch (_) {467
/* Best effort. */468
}469
rethrow;470
}471
await file.close();472
return client.stat(path, followLink: false);473
}474
}SHA-256
a36aa268aa18f169cf10c9076ff9951ee2f03ce4f582bf7ab9ba0284141c3093CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655