Files
plezy/packages/shared_preferences_windows/lib/shared_preferences_windows.dart
T
edde746 8e1be64d8d fix(windows): rewrite the preference store in place when a reader vetoes the rename
dart:io opens files without FILE_SHARE_DELETE, so MoveFileExW with
MOVEFILE_REPLACE_EXISTING - and any other replacement strategy - fails
while such a reader holds the document open. Fall back to the upstream
in-place rewrite so a hostile reader costs at most crash-atomicity for
that one write instead of silently dropping it, and sweep the staging
copy the rename did not consume.
2026-08-09 18:51:47 +02:00

479 lines
16 KiB
Dart

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert' show json;
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter/foundation.dart' show debugPrint, visibleForTesting;
import 'package:path/path.dart' as path;
import 'package:path_provider_windows/path_provider_windows.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';
const String _defaultFileName = 'shared_preferences';
const String _defaultPrefix = 'flutter.';
/// The Windows implementation of [SharedPreferencesStorePlatform].
///
/// This class implements the `package:shared_preferences` functionality for Windows.
class SharedPreferencesWindows extends SharedPreferencesStorePlatform {
/// Deprecated instance of [SharedPreferencesWindows].
/// Use [SharedPreferencesStorePlatform.instance] instead.
@Deprecated('Use `SharedPreferencesStorePlatform.instance` instead.')
static SharedPreferencesWindows instance = SharedPreferencesWindows();
/// Registers the Windows implementation.
static void registerWith() {
SharedPreferencesStorePlatform.instance = SharedPreferencesWindows();
// A temporary work-around for having two plugins contained in a single package.
SharedPreferencesAsyncWindows.registerWith();
}
/// Local copy of preferences
Map<String, Object>? _cachedPreferences;
/// File system used to store to disk. Exposed for testing only.
@visibleForTesting
FileSystem fs = const LocalFileSystem();
/// The path_provider_windows instance used to find the support directory.
@visibleForTesting
PathProviderWindows pathProvider = PathProviderWindows();
/// Checks for cached preferences and returns them or loads preferences from
/// file and returns and caches them.
Future<Map<String, Object>> _readPreferences() async {
_cachedPreferences ??= await _readFromFile(
_defaultFileName,
fs: fs,
pathProvider: pathProvider,
);
return _cachedPreferences!;
}
@override
Future<bool> clear() async {
return clearWithParameters(
ClearParameters(
filter: PreferencesFilter(prefix: _defaultPrefix),
),
);
}
@override
Future<bool> clearWithPrefix(String prefix) async {
return clearWithParameters(ClearParameters(filter: PreferencesFilter(prefix: prefix)));
}
@override
Future<bool> clearWithParameters(ClearParameters parameters) async {
final PreferencesFilter filter = parameters.filter;
final Map<String, Object> preferences = await _readPreferences();
preferences.removeWhere((String key, _) =>
key.startsWith(filter.prefix) && (filter.allowList == null || filter.allowList!.contains(key)));
return _writePreferences(
preferences,
_defaultFileName,
fs: fs,
pathProvider: pathProvider,
);
}
@override
Future<Map<String, Object>> getAll() async {
return getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: _defaultPrefix),
),
);
}
@override
Future<Map<String, Object>> getAllWithPrefix(String prefix) async {
return getAllWithParameters(GetAllParameters(filter: PreferencesFilter(prefix: prefix)));
}
@override
Future<Map<String, Object>> getAllWithParameters(GetAllParameters parameters) async {
final PreferencesFilter filter = parameters.filter;
final Map<String, Object> withPrefix = Map<String, Object>.from(await _readPreferences());
withPrefix
.removeWhere((String key, _) => !(key.startsWith(filter.prefix) && (filter.allowList?.contains(key) ?? true)));
return withPrefix;
}
@override
Future<bool> remove(String key) async {
final Map<String, Object> preferences = await _readPreferences();
preferences.remove(key);
return _writePreferences(
preferences,
_defaultFileName,
fs: fs,
pathProvider: pathProvider,
);
}
@override
Future<bool> setValue(String valueType, String key, Object value) async {
final Map<String, Object> preferences = await _readPreferences();
preferences[key] = value;
return _writePreferences(
preferences,
_defaultFileName,
fs: fs,
pathProvider: pathProvider,
);
}
}
/// The Windows implementation of [SharedPreferencesAsyncPlatform].
///
/// This class implements the `package:shared_preferences` functionality for Windows.
base class SharedPreferencesAsyncWindows extends SharedPreferencesAsyncPlatform {
/// Registers the Windows implementation.
static void registerWith() {
SharedPreferencesAsyncPlatform.instance = SharedPreferencesAsyncWindows();
}
/// Local copy of preferences
Map<String, Object>? _cachedPreferences;
/// File system used to store to disk. Exposed for testing only.
@visibleForTesting
FileSystem fs = const LocalFileSystem();
/// The path_provider_windows instance used to find the support directory.
@visibleForTesting
PathProviderWindows pathProvider = PathProviderWindows();
@override
Future<Set<String>> getKeys(
GetPreferencesParameters parameters,
SharedPreferencesOptions options,
) async {
return (await getPreferences(parameters, options)).keys.toSet();
}
@override
Future<void> setString(
String key,
String value,
SharedPreferencesOptions options,
) {
return _setValue(key, value, options);
}
@override
Future<void> setBool(
String key,
bool value,
SharedPreferencesOptions options,
) {
return _setValue(key, value, options);
}
@override
Future<void> setDouble(
String key,
double value,
SharedPreferencesOptions options,
) {
return _setValue(key, value, options);
}
@override
Future<void> setInt(
String key,
int value,
SharedPreferencesOptions options,
) {
return _setValue(key, value, options);
}
@override
Future<void> setStringList(
String key,
List<String> value,
SharedPreferencesOptions options,
) {
return _setValue(key, value, options);
}
@override
Future<String?> getString(
String key,
SharedPreferencesOptions options,
) async {
final Map<String, Object> data = await _readAll(<String>{key}, options);
return data[key] as String?;
}
@override
Future<bool?> getBool(
String key,
SharedPreferencesOptions options,
) async {
final Map<String, Object> data = await _readAll(<String>{key}, options);
return data[key] as bool?;
}
@override
Future<double?> getDouble(
String key,
SharedPreferencesOptions options,
) async {
final Map<String, Object> data = await _readAll(<String>{key}, options);
return data[key] as double?;
}
@override
Future<int?> getInt(
String key,
SharedPreferencesOptions options,
) async {
final Map<String, Object> data = await _readAll(<String>{key}, options);
return data[key] as int?;
}
@override
Future<List<String>?> getStringList(
String key,
SharedPreferencesOptions options,
) async {
final Map<String, Object> data = await _readAll(<String>{key}, options);
return (data[key] as List<Object?>?)?.cast<String>().toList();
}
@override
Future<void> clear(ClearPreferencesParameters parameters, SharedPreferencesOptions options) async {
final SharedPreferencesWindowsOptions windowsOptions =
SharedPreferencesWindowsOptions.fromSharedPreferencesOptions(options);
final PreferencesFilters filter = parameters.filter;
final Map<String, Object> preferences = await _readPreferences(windowsOptions.fileName);
preferences.removeWhere((String key, _) => filter.allowList == null || filter.allowList!.contains(key));
await _writePreferences(
preferences,
windowsOptions.fileName,
fs: fs,
pathProvider: pathProvider,
);
}
@override
Future<Map<String, Object>> getPreferences(
GetPreferencesParameters parameters,
SharedPreferencesOptions options,
) async {
return _readAll(parameters.filter.allowList, options);
}
/// Reloads preferences from file.
@visibleForTesting
Future<void> reload(
SharedPreferencesWindowsOptions options,
) async {
_cachedPreferences = await _readFromFile(options.fileName);
}
Future<Map<String, Object>> _readAll(
Set<String>? allowList,
SharedPreferencesOptions options,
) async {
final SharedPreferencesWindowsOptions windowsOptions =
SharedPreferencesWindowsOptions.fromSharedPreferencesOptions(options);
final Map<String, Object> prefs = Map<String, Object>.from(await _readPreferences(windowsOptions.fileName));
prefs.removeWhere((String key, _) => !(allowList?.contains(key) ?? true));
return prefs;
}
Future<void> _setValue(String key, Object value, SharedPreferencesOptions options) async {
final SharedPreferencesWindowsOptions windowsOptions =
SharedPreferencesWindowsOptions.fromSharedPreferencesOptions(options);
final Map<String, Object> preferences = await _readPreferences(windowsOptions.fileName);
preferences[key] = value;
await _writePreferences(
preferences,
windowsOptions.fileName,
fs: fs,
pathProvider: pathProvider,
);
}
/// Checks for cached preferences and returns them or loads preferences from
/// file and returns and caches them.
Future<Map<String, Object>> _readPreferences(String fileName) async {
_cachedPreferences ??= await _readFromFile(
fileName,
fs: fs,
pathProvider: pathProvider,
);
return _cachedPreferences!;
}
}
/// Gets the file where the preferences are stored.
Future<File?> _getLocalDataFile(
String fileName, {
FileSystem fs = const LocalFileSystem(),
PathProviderWindows? pathProvider,
}) async {
pathProvider = pathProvider ?? PathProviderWindows();
final String? directory = await pathProvider.getApplicationSupportPath();
if (directory == null) {
return null;
}
final String fileLocation = path.join(directory, '$fileName.json');
return fs.file(fileLocation);
}
/// Suffix of the staging file used by the atomic write below.
///
/// PLEZY DELTA. Deliberately a single fixed name rather than a stamped one:
/// this file holds the whole preference document, credentials included, so at
/// most one may ever exist and a later write must reuse it rather than
/// accumulate copies.
const String _stagingSuffix = '.tmp';
/// Gets the preferences from the stored file.
Future<Map<String, Object>> _readFromFile(
String fileName, {
FileSystem fs = const LocalFileSystem(),
PathProviderWindows? pathProvider,
}) async {
Map<String, Object> preferences = <String, Object>{};
final File? localDataFile = await _getLocalDataFile(
fileName,
fs: fs,
pathProvider: pathProvider,
);
if (localDataFile != null && localDataFile.existsSync()) {
final String stringMap = localDataFile.readAsStringSync();
if (stringMap.isNotEmpty) {
final Object? data = json.decode(stringMap);
if (data is Map) {
preferences = data.cast<String, Object>();
}
}
// PLEZY DELTA: the canonical document just read cleanly, so any staging
// file left by an interrupted write is stale. It is a plaintext copy of
// the credentials, so it does not get to sit there indefinitely.
_removeStagingFile(localDataFile);
}
return preferences;
}
/// Deletes a stale staging file, best effort.
///
/// PLEZY DELTA. Never allowed to fail a read: the staging file is ours, and a
/// locked or already-removed one changes nothing about the document.
void _removeStagingFile(File localDataFile) {
try {
final File staged = localDataFile.fileSystem.file('${localDataFile.path}$_stagingSuffix');
if (staged.existsSync()) {
staged.deleteSync();
}
} catch (e) {
debugPrint('Could not remove a stale preferences staging file: $e');
}
}
/// Writes the cached preferences to disk. Returns [true] if the operation
/// succeeded.
Future<bool> _writePreferences(
Map<String, Object> preferences,
String fileName, {
FileSystem fs = const LocalFileSystem(),
PathProviderWindows? pathProvider,
}) async {
try {
final File? localDataFile = await _getLocalDataFile(
fileName,
fs: fs,
pathProvider: pathProvider,
);
if (localDataFile == null) {
debugPrint('Unable to determine where to write preferences.');
return false;
}
final String stringMap = json.encode(preferences);
// PLEZY DELTA: stage, flush, then rename over the target.
//
// Upstream calls `writeAsStringSync` straight onto the live document.
// That opens with the default `FileMode.write`, which truncates first, so
// every single preference write has a window in which the only copy of the
// store on disk is empty or half-written. A crash, power loss, forced
// reboot or antivirus interception inside that window leaves a document
// that fails to parse on every subsequent launch, and the store holds the
// credential-vault key, so the damage is not recoverable by rewriting it.
//
// Renaming into place is atomic for readers: `MoveFileExW` with
// MOVEFILE_REPLACE_EXISTING on Windows, `rename(2)` elsewhere. The flush
// has to come first, or the rename could publish a file whose contents
// were never committed — the same corruption by a different route.
//
// The staging file is created in the target's own directory, both because
// rename must stay on one volume and so it inherits exactly the mode the
// canonical file would have been created with.
final File staged = localDataFile.fileSystem.file('${localDataFile.path}$_stagingSuffix');
if (!staged.parent.existsSync()) {
staged.parent.createSync(recursive: true);
}
staged.writeAsStringSync(stringMap, flush: true);
try {
staged.renameSync(localDataFile.path);
} on FileSystemException {
// PLEZY DELTA: a reader that denies delete sharing vetoes every rename
// over the document. `MoveFileExW` with MOVEFILE_REPLACE_EXISTING — and
// even a POSIX-semantics rename — fails while an open handle on the
// destination lacks FILE_SHARE_DELETE, and dart:io's own `File.open`
// produces exactly such a handle (`_wopen` shares read/write only), as
// do some antivirus and indexing services. The atomic path is not slow
// there; it is unavailable.
//
// Fall back to the upstream in-place rewrite: the sharing mode that
// blocks the rename still admits opening the document for write. This
// reintroduces the truncate window only while such a reader holds the
// store — a brief loss of crash-atomicity beats turning every
// preference write into a silent no-op on exactly the machines whose
// scanners are most likely to have damaged the store in the first
// place.
localDataFile.writeAsStringSync(stringMap, flush: true);
// The staging copy holds the whole document, credentials included, and
// was not consumed by a rename, so it must not outlive the write.
_removeStagingFile(localDataFile);
}
} catch (e) {
debugPrint('Error saving preferences to disk: $e');
return false;
}
return true;
}
/// Windows specific SharedPreferences Options.
class SharedPreferencesWindowsOptions extends SharedPreferencesOptions {
/// Constructor for SharedPreferencesWindowsOptions.
const SharedPreferencesWindowsOptions({
this.fileName = 'shared_preferences', // Same as current defaults.
});
/// The name of the file to store preferences in.
final String fileName;
/// Returns a new instance of [SharedPreferencesWindowsOptions] from an existing
/// [SharedPreferencesOptions].
static SharedPreferencesWindowsOptions fromSharedPreferencesOptions(SharedPreferencesOptions options) {
if (options is SharedPreferencesWindowsOptions) {
return options;
}
return const SharedPreferencesWindowsOptions();
}
}