fix(prefs): replace the desktop preference store atomically

Upstream shared_preferences_windows and _linux write the whole preference
document with a bare `writeAsStringSync`. That opens with the default
`FileMode.write`, which truncates the live file before writing it, so every
single preference write has a window in which the only copy 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 loss is not
recoverable by rewriting it. This is the corruption class behind #1732; the
recovery path already landed is a band-aid over it.

Vendor both packages under packages/ — the convention saf_util and
wakelock_plus already follow — and stage, flush, then rename over the target.
The flush has to precede the rename or it could publish contents that were
never committed, the same corruption by another route. Staging uses one fixed
sibling name rather than a stamped one, because the file is a plaintext copy
of the vault key, tracker refresh tokens and Seerr cookies; it is created in
the target's own directory so rename stays on one volume and the mode matches
what the canonical file would have had, and a stale one is swept once the
canonical document has been read cleanly. Both deltas are marked in-source and
in provenance.json with the refresh contract.

Atomicity is proven, not asserted. A hard link to the store observes the old
document after a write, which only holds when the directory entry was replaced
— truncate-in-place would have rewritten the shared inode, and that test does
fail against unpatched upstream. Upstream's own suites still pass unchanged in
both packages and now run in CI, so the patch keeps the contract it inherited.
Windows `MoveFileExW` replacement semantics cannot be proven on a POSIX runner
or a memory file system, so they get their own test on the existing
windows-latest job, including replacement while a reader holds the file open —
antivirus and Search Indexer both do.
This commit is contained in:
edde746
2026-08-01 06:59:20 +02:00
parent 9ecf8db90f
commit 3f49bcabf8
43 changed files with 3243 additions and 13 deletions
+17
View File
@@ -147,6 +147,14 @@ jobs:
working-directory: packages/wakelock_plus
run: flutter test --platform chrome --dart-define=WEB_PLUGIN_TESTS=true test/wakelock_plus_web_plugin_test.dart
# The vendored atomic-write patch has to keep the upstream contract, not
# just the new behaviour. Upstream's own suites are the check for that.
- name: Run the vendored desktop preference store tests
run: |
for pkg in shared_preferences_linux shared_preferences_windows; do
(cd "packages/$pkg" && flutter pub get --enforce-lockfile && flutter test)
done
android-test:
name: Android JVM and Native Tests
runs-on: ubuntu-latest
@@ -471,6 +479,15 @@ jobs:
shell: pwsh
run: flutter pub get --enforce-lockfile --no-example
# Windows replacement semantics (`MoveFileExW` with
# MOVEFILE_REPLACE_EXISTING) cannot be proven on the POSIX runners, and a
# memory file system proves nothing about either. The vendored
# shared_preferences_windows store write is the fix for #1732, so exercise
# it here, on a real NTFS volume, against the real backend.
- name: Run the vendored Windows preference store tests
shell: pwsh
run: flutter test test/services/prefs_store_atomic_write_windows_test.dart
- name: Install patched Flutter engine
shell: pwsh
run: |
+66
View File
@@ -0,0 +1,66 @@
# Below is a list of people and organizations that have contributed
# to the Flutter project. Names should be added to the list like so:
#
# Name/Organization <email address>
Google Inc.
The Chromium Authors
German Saprykin <saprykin.h@gmail.com>
Benjamin Sauer <sauer.benjamin@gmail.com>
larsenthomasj@gmail.com
Ali Bitek <alibitek@protonmail.ch>
Pol Batlló <pol.batllo@gmail.com>
Anatoly Pulyaevskiy
Hayden Flinner <haydenflinner@gmail.com>
Stefano Rodriguez <hlsroddy@gmail.com>
Salvatore Giordano <salvatoregiordanoo@gmail.com>
Brian Armstrong <brian@flutter.institute>
Paul DeMarco <paulmdemarco@gmail.com>
Fabricio Nogueira <feufeu@gmail.com>
Simon Lightfoot <simon@devangels.london>
Ashton Thomas <ashton@acrinta.com>
Thomas Danner <thmsdnnr@gmail.com>
Diego Velásquez <diego.velasquez.lopez@gmail.com>
Hajime Nakamura <nkmrhj@gmail.com>
Tuyển Vũ Xuân <netsoft1985@gmail.com>
Miguel Ruivo <miguel@miguelruivo.com>
Sarthak Verma <sarthak@artiosys.com>
Mike Diarmid <mike@invertase.io>
Invertase <oss@invertase.io>
Elliot Hesp <elliot@invertase.io>
Vince Varga <vince.varga@smaho.com>
Aawaz Gyawali <awazgyawali@gmail.com>
EUI Limited <ian.evans3@admiralgroup.co.uk>
Katarina Sheremet <katarina@sheremet.ch>
Thomas Stockx <thomas@stockxit.com>
Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
Ozkan Eksi <ozeksi@gmail.com>
Rishab Nayak <rishab@bu.edu>
ko2ic <ko2ic.dev@gmail.com>
Jonathan Younger <jonathan@daikini.com>
Jose Sanchez <josesm82@gmail.com>
Debkanchan Samadder <debu.samadder@gmail.com>
Audrius Karosevicius <audrius.karosevicius@gmail.com>
Lukasz Piliszczuk <lukasz@intheloup.io>
SoundReply Solutions GmbH <ch@soundreply.com>
Rafal Wachol <rwachol@gmail.com>
Pau Picas <pau.picas@gmail.com>
Christian Weder <chrstian.weder@yapeal.ch>
Alexandru Tuca <salexandru.tuca@outlook.com>
Christian Weder <chrstian.weder@yapeal.ch>
Rhodes Davis Jr. <rody.davis.jr@gmail.com>
Luigi Agosti <luigi@tengio.com>
Quentin Le Guennec <quentin@tengio.com>
Koushik Ravikumar <koushik@tengio.com>
Nissim Dsilva <nissim@tengio.com>
Giancarlo Rocha <giancarloiff@gmail.com>
Ryo Miyake <ryo@miyake.id>
Théo Champion <contact.theochampion@gmail.com>
Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
Eitan Schwartz <eshvartz@gmail.com>
Chris Rutkowski <chrisrutkowski89@gmail.com>
Juan Alvarez <juan.alvarez@resideo.com>
Aleksandr Yurkovskiy <sanekyy@gmail.com>
Anton Borries <mail@antonborri.es>
Alex Li <google@alexv525.com>
Rahul Raj <64.rahulraj@gmail.com>
@@ -0,0 +1,127 @@
## 2.4.1 (Plezy vendored patch)
Vendored from pub.dev `shared_preferences_linux` 2.4.1. Upstream writes the whole
preference document with a bare `writeAsStringSync`, which truncates the live
file before writing it, so every preference write has a window in which the
only copy on disk is empty or half-written. A crash inside that window leaves a
store that fails to parse on every later launch — and it holds the
credential-vault key, so the loss is not recoverable by rewriting it. This is
the corruption behind Plezy issue #1732.
The local patch stages the document to a sibling `.tmp`, flushes it, then
renames it over the target, and sweeps a stale staging file once the canonical
document has been read cleanly. The published example app is not vendored.
When updating, reapply both PLEZY DELTA blocks in `lib/shared_preferences_linux.dart`
and see `provenance.json` for the full refresh contract.
## 2.4.1
* Fixes `getStringList` returning immutable list.
* Fixes `getStringList` cast error.
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
## 2.4.0
* Adds `SharedPreferencesAsyncLinux` API.
* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.
## 2.3.2
* Updates `package:file` version constraints.
## 2.3.1
* Adds pub topics to package metadata.
* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
## 2.3.0
* Adds `clearWithParameters` and `getAllWithParameters` methods.
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
## 2.2.0
* Adds `getAllWithPrefix` and `clearWithPrefix` methods.
## 2.1.5
* Clarifies explanation of endorsement in README.
* Aligns Dart and Flutter SDK constraints.
## 2.1.4
* Updates links for the merge of flutter/plugins into flutter/packages.
* Updates minimum Flutter version to 3.0.
## 2.1.3
* Updates code for stricter lint checks.
## 2.1.2
* Updates code for stricter lint checks.
* Updates code for `no_leading_underscores_for_local_identifiers` lint.
* Updates minimum Flutter version to 2.10.
## 2.1.1
* Removes unnecessary imports.
* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors
lint warnings.
## 2.1.0
* Deprecated `SharedPreferencesWindows.instance` in favor of `SharedPreferencesStorePlatform.instance`.
## 2.0.4
* Removes dependency on `meta`.
## 2.0.3
* Removed obsolete `pluginClass: none` from pubpsec.
* Fixes newly enabled analyzer options.
## 2.0.2
* Updated installation instructions in README.
## 2.0.1
* Add `implements` to the pubspec.
* Add `registerWith` to the Dart main class.
## 2.0.0
* Migrate to null-safety.
## 0.0.3+1
* Update Flutter SDK constraint.
## 0.0.3
* Update integration test examples to use `testWidgets` instead of `test`.
## 0.0.2+4
* Remove unused `test` dependency.
* Update Dart SDK constraint in example.
## 0.0.2+3
* Check in linux/ directory for example/
## 0.0.2+2
* Bump the `file` package dependency to resolve dep conflicts with `flutter_driver`.
## 0.0.2+1
* Replace path_provider dependency with path_provider_linux.
## 0.0.2
* Add iOS stub.
## 0.0.1
* Initial release to support shared_preferences on Linux.
+25
View File
@@ -0,0 +1,25 @@
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,15 @@
# shared\_preferences\_linux
The Linux implementation of [`shared_preferences`][1].
## Usage
This package is [endorsed][2], which means you can simply use `shared_preferences`
normally. This package will be automatically included in your app when you do,
so you do not need to add it to your `pubspec.yaml`.
However, if you `import` this package to use any of its APIs directly, you
should add it to your `pubspec.yaml` as usual.
[1]: https://pub.dev/packages/shared_preferences
[2]: https://flutter.dev/to/endorsed-federated-plugin
@@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}
@@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}
@@ -0,0 +1,456 @@
// 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_linux/path_provider_linux.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 Linux implementation of [SharedPreferencesStorePlatform].
///
/// This class implements the `package:shared_preferences` functionality for Linux.
class SharedPreferencesLinux extends SharedPreferencesStorePlatform {
/// Deprecated instance of [SharedPreferencesLinux].
/// Use [SharedPreferencesStorePlatform.instance] instead.
@Deprecated('Use `SharedPreferencesStorePlatform.instance` instead.')
static SharedPreferencesLinux instance = SharedPreferencesLinux();
/// Registers the Linux implementation.
static void registerWith() {
SharedPreferencesStorePlatform.instance = SharedPreferencesLinux();
// A temporary work-around for having two plugins contained in a single package.
SharedPreferencesAsyncLinux.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_linux instance used to find the support directory.
@visibleForTesting
PathProviderLinux pathProvider = PathProviderLinux();
/// 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 _reload(
_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 Linux implementation of [SharedPreferencesAsyncPlatform].
///
/// This class implements the `package:shared_preferences` functionality for Linux.
base class SharedPreferencesAsyncLinux extends SharedPreferencesAsyncPlatform {
/// Registers the Linux implementation.
static void registerWith() {
SharedPreferencesAsyncPlatform.instance = SharedPreferencesAsyncLinux();
}
/// 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_linux instance used to find the support directory.
@visibleForTesting
PathProviderLinux pathProvider = PathProviderLinux();
@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 SharedPreferencesLinuxOptions linuxOptions =
SharedPreferencesLinuxOptions.fromSharedPreferencesOptions(options);
final PreferencesFilters filter = parameters.filter;
final Map<String, Object> preferences = await _readPreferences(linuxOptions.fileName);
preferences.removeWhere((String key, _) => filter.allowList == null || filter.allowList!.contains(key));
await _writePreferences(
preferences,
linuxOptions.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(
SharedPreferencesLinuxOptions options,
) async {
_cachedPreferences = await _reload(options.fileName);
}
Future<Map<String, Object>> _readAll(
Set<String>? allowList,
SharedPreferencesOptions options,
) async {
final SharedPreferencesLinuxOptions linuxOptions =
SharedPreferencesLinuxOptions.fromSharedPreferencesOptions(options);
final Map<String, Object> prefs = Map<String, Object>.from(await _readPreferences(linuxOptions.fileName));
prefs.removeWhere((String key, _) => !(allowList?.contains(key) ?? true));
return prefs;
}
Future<void> _setValue(String key, Object value, SharedPreferencesOptions options) async {
final SharedPreferencesLinuxOptions linuxOptions =
SharedPreferencesLinuxOptions.fromSharedPreferencesOptions(options);
final Map<String, Object> preferences = await _readPreferences(linuxOptions.fileName);
preferences[key] = value;
await _writePreferences(
preferences,
linuxOptions.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 _reload(
fileName,
fs: fs,
pathProvider: pathProvider,
);
return _cachedPreferences!;
}
}
/// Gets the file where the preferences are stored.
Future<File?> _getLocalDataFile(
String fileName, {
FileSystem fs = const LocalFileSystem(),
PathProviderLinux? pathProvider,
}) async {
pathProvider = pathProvider ?? PathProviderLinux();
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 and saves them in cache.
Future<Map<String, Object>> _reload(
String fileName, {
FileSystem fs = const LocalFileSystem(),
PathProviderLinux? 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(),
PathProviderLinux? 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: `rename(2)` here,
// `MoveFileExW` with MOVEFILE_REPLACE_EXISTING on Windows. 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);
staged.renameSync(localDataFile.path);
} catch (e) {
debugPrint('Error saving preferences to disk: $e');
return false;
}
return true;
}
/// Linux specific SharedPreferences Options.
class SharedPreferencesLinuxOptions extends SharedPreferencesOptions {
/// Constructor for SharedPreferencesLinuxOptions.
const SharedPreferencesLinuxOptions({
this.fileName = 'shared_preferences',
});
/// The name of the file to store preferences in.
final String fileName;
/// Returns a new instance of [SharedPreferencesLinuxOptions] from an existing
/// [SharedPreferencesOptions].
static SharedPreferencesLinuxOptions fromSharedPreferencesOptions(SharedPreferencesOptions options) {
if (options is SharedPreferencesLinuxOptions) {
return options;
}
return const SharedPreferencesLinuxOptions();
}
}
@@ -0,0 +1,20 @@
{
"formatVersion": 1,
"upstream": {
"repository": "https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_linux",
"packageVersion": "2.4.1",
"source": "pub.dev"
},
"plezyDeltas": [
"atomic store replacement: stage to a sibling .tmp, flush, then rename over the target",
"stale staging files are swept once the canonical document has been read cleanly",
"the published example app is not vendored"
],
"refreshContract": [
"Diff against the new upstream version and reapply every PLEZY DELTA block in lib/shared_preferences_linux.dart verbatim.",
"Never reintroduce a bare writeAsStringSync onto the live document: it truncates before writing, which is the corruption in issue #1732.",
"Keep the staging name fixed and single. The file is a plaintext copy of the credential-vault key, tracker refresh tokens and Seerr cookies; stamped names would accumulate them.",
"Keep the flush before the rename. A rename can otherwise publish contents that were never committed.",
"Run flutter test test/services/prefs_store_atomic_write_test.dart from the repository root, and the real-Windows step in .github/workflows/ci.yml."
]
}
@@ -0,0 +1,253 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: "direct main"
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
dependency: "direct main"
description:
name: path_provider_linux
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_platform_interface:
dependency: "direct main"
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
shared_preferences_platform_interface:
dependency: "direct main"
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"
@@ -0,0 +1,34 @@
name: shared_preferences_linux
description: Linux implementation of the shared_preferences plugin
repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_linux
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22
version: 2.4.1
environment:
sdk: ^3.3.0
flutter: ">=3.19.0"
flutter:
plugin:
implements: shared_preferences
platforms:
linux:
dartPluginClass: SharedPreferencesLinux
dependencies:
file: ">=6.0.0 <8.0.0"
flutter:
sdk: flutter
path: ^1.8.0
path_provider_linux: ^2.0.0
path_provider_platform_interface: ^2.0.0
shared_preferences_platform_interface: ^2.4.0
dev_dependencies:
flutter_test:
sdk: flutter
topics:
- persistence
- shared-preferences
- storage
@@ -0,0 +1,29 @@
// 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 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_linux/path_provider_linux.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
/// Fake implementation of PathProviderLinux that returns hard-coded paths,
/// allowing tests to run on any platform.
///
/// Note that this should only be used with an in-memory filesystem, as the
/// path it returns is a root path that does not actually exist on Linux.
class FakePathProviderLinux extends PathProviderPlatform
implements PathProviderLinux {
@override
Future<String?> getApplicationSupportPath() async => r'/appsupport';
@override
Future<String?> getTemporaryPath() async => null;
@override
Future<String?> getLibraryPath() async => null;
@override
Future<String?> getApplicationDocumentsPath() async => null;
@override
Future<String?> getDownloadsPath() async => null;
}
@@ -0,0 +1,257 @@
// 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:convert';
import 'package:file/memory.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider_linux/path_provider_linux.dart';
import 'package:shared_preferences_linux/shared_preferences_linux.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';
import 'fake_path_provider_linux.dart';
void main() {
late MemoryFileSystem fs;
late PathProviderLinux pathProvider;
SharedPreferencesLinux.registerWith();
const Map<String, Object> flutterTestValues = <String, Object>{
'flutter.String': 'hello world',
'flutter.Bool': true,
'flutter.Int': 42,
'flutter.Double': 3.14159,
'flutter.StringList': <String>['foo', 'bar'],
};
const Map<String, Object> prefixTestValues = <String, Object>{
'prefix.String': 'hello world',
'prefix.Bool': true,
'prefix.Int': 42,
'prefix.Double': 3.14159,
'prefix.StringList': <String>['foo', 'bar'],
};
const Map<String, Object> nonPrefixTestValues = <String, Object>{
'String': 'hello world',
'Bool': true,
'Int': 42,
'Double': 3.14159,
'StringList': <String>['foo', 'bar'],
};
final Map<String, Object> allTestValues = <String, Object>{};
allTestValues.addAll(flutterTestValues);
allTestValues.addAll(prefixTestValues);
allTestValues.addAll(nonPrefixTestValues);
setUp(() {
fs = MemoryFileSystem.test();
pathProvider = FakePathProviderLinux();
});
Future<String> getFilePath() async {
final String? directory = await pathProvider.getApplicationSupportPath();
return path.join(directory!, 'shared_preferences.json');
}
Future<void> writeTestFile(String value) async {
fs.file(await getFilePath())
..createSync(recursive: true)
..writeAsStringSync(value);
}
Future<String> readTestFile() async {
return fs.file(await getFilePath()).readAsStringSync();
}
SharedPreferencesLinux getPreferences() {
final SharedPreferencesLinux prefs = SharedPreferencesLinux();
prefs.fs = fs;
prefs.pathProvider = pathProvider;
return prefs;
}
test('registered instance', () async {
SharedPreferencesLinux.registerWith();
expect(
SharedPreferencesStorePlatform.instance, isA<SharedPreferencesLinux>());
});
test('getAll', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesLinux prefs = getPreferences();
final Map<String, Object> values = await prefs.getAll();
expect(values, hasLength(5));
expect(values, flutterTestValues);
});
test('getAllWithPrefix', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesLinux prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithPrefix('prefix.');
expect(values, hasLength(5));
expect(values, prefixTestValues);
});
test('getAllWithParameters', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesLinux prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
expect(values, hasLength(5));
expect(values, prefixTestValues);
});
test('getAllWithParameters with allow list', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesLinux prefs = getPreferences();
final Map<String?, Object?> all = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(
prefix: 'prefix.',
allowList: <String>{'prefix.Bool'},
),
),
);
expect(all.length, 1);
expect(all['prefix.Bool'], prefixTestValues['prefix.Bool']);
});
test('remove', () async {
await writeTestFile('{"key1":"one","key2":2}');
final SharedPreferencesLinux prefs = getPreferences();
await prefs.remove('key2');
expect(await readTestFile(), '{"key1":"one"}');
});
test('setValue', () async {
await writeTestFile('{}');
final SharedPreferencesLinux prefs = getPreferences();
await prefs.setValue('', 'key1', 'one');
await prefs.setValue('', 'key2', 2);
expect(await readTestFile(), '{"key1":"one","key2":2}');
});
test('clear', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesLinux prefs = getPreferences();
expect(await readTestFile(), json.encode(flutterTestValues));
await prefs.clear();
expect(await readTestFile(), '{}');
});
test('clearWithPrefix', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesLinux prefs = getPreferences();
await prefs.clearWithPrefix('prefix.');
final Map<String, Object> noValues =
await prefs.getAllWithPrefix('prefix.');
expect(noValues, hasLength(0));
final Map<String, Object> values = await prefs.getAll();
expect(values, hasLength(5));
expect(values, flutterTestValues);
});
test('getAllWithNoPrefix', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesLinux prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithPrefix('');
expect(values, hasLength(15));
expect(values, allTestValues);
});
test('clearWithNoPrefix', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesLinux prefs = getPreferences();
await prefs.clearWithPrefix('');
final Map<String, Object> noValues = await prefs.getAllWithPrefix('');
expect(noValues, hasLength(0));
});
test('clearWithParameters', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesLinux prefs = getPreferences();
await prefs.clearWithParameters(
ClearParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
final Map<String, Object> noValues = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
expect(noValues, hasLength(0));
final Map<String, Object> values = await prefs.getAll();
expect(values, hasLength(5));
expect(values, flutterTestValues);
});
test('clearWithParameters with allow list', () async {
await writeTestFile(json.encode(prefixTestValues));
final SharedPreferencesLinux prefs = getPreferences();
await prefs.clearWithParameters(
ClearParameters(
filter: PreferencesFilter(
prefix: 'prefix.',
allowList: <String>{'prefix.StringList'},
),
),
);
final Map<String, Object> someValues = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
expect(someValues, hasLength(4));
});
test('getAllWithNoPrefix', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesLinux prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: ''),
),
);
expect(values, hasLength(15));
expect(values, allTestValues);
});
test('clearWithNoPrefix', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesLinux prefs = getPreferences();
await prefs.clearWithParameters(
ClearParameters(
filter: PreferencesFilter(prefix: ''),
),
);
final Map<String, Object> noValues = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: ''),
),
);
expect(noValues, hasLength(0));
});
}
@@ -0,0 +1,203 @@
// 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 'package:file/memory.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_linux/path_provider_linux.dart';
import 'package:shared_preferences_linux/shared_preferences_linux.dart';
import 'package:shared_preferences_platform_interface/types.dart';
import 'fake_path_provider_linux.dart';
void main() {
late MemoryFileSystem fs;
late PathProviderLinux pathProvider;
SharedPreferencesAsyncLinux.registerWith();
const String stringKey = 'testString';
const String boolKey = 'testBool';
const String intKey = 'testInt';
const String doubleKey = 'testDouble';
const String listKey = 'testList';
const String testString = 'hello world';
const bool testBool = true;
const int testInt = 42;
const double testDouble = 3.14159;
const List<String> testList = <String>['foo', 'bar'];
const SharedPreferencesLinuxOptions emptyOptions =
SharedPreferencesLinuxOptions();
setUp(() {
fs = MemoryFileSystem.test();
pathProvider = FakePathProviderLinux();
});
SharedPreferencesAsyncLinux getPreferences() {
final SharedPreferencesAsyncLinux prefs = SharedPreferencesAsyncLinux();
prefs.fs = fs;
prefs.pathProvider = pathProvider;
return prefs;
}
test('set and get String', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
expect(await preferences.getString(stringKey, emptyOptions), testString);
});
test('set and get bool', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setBool(boolKey, testBool, emptyOptions);
expect(await preferences.getBool(boolKey, emptyOptions), testBool);
});
test('set and get int', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setInt(intKey, testInt, emptyOptions);
expect(await preferences.getInt(intKey, emptyOptions), testInt);
});
test('set and get double', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
expect(await preferences.getDouble(doubleKey, emptyOptions), testDouble);
});
test('set and get StringList', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setStringList(listKey, testList, emptyOptions);
expect(await preferences.getStringList(listKey, emptyOptions), testList);
});
test('getPreferences', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Map<String, Object?> gotAll = await preferences.getPreferences(
const GetPreferencesParameters(filter: PreferencesFilters()),
emptyOptions);
expect(gotAll.length, 5);
expect(gotAll[stringKey], testString);
expect(gotAll[boolKey], testBool);
expect(gotAll[intKey], testInt);
expect(gotAll[doubleKey], testDouble);
expect(gotAll[listKey], testList);
});
test('getPreferences with filter', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Map<String, Object?> gotAll = await preferences.getPreferences(
const GetPreferencesParameters(
filter:
PreferencesFilters(allowList: <String>{stringKey, boolKey})),
emptyOptions);
expect(gotAll.length, 2);
expect(gotAll[stringKey], testString);
expect(gotAll[boolKey], testBool);
});
test('getKeys', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Set<String> keys = await preferences.getKeys(
const GetPreferencesParameters(filter: PreferencesFilters()),
emptyOptions,
);
expect(keys.length, 5);
expect(keys, contains(stringKey));
expect(keys, contains(boolKey));
expect(keys, contains(intKey));
expect(keys, contains(doubleKey));
expect(keys, contains(listKey));
});
test('getKeys with filter', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Set<String> keys = await preferences.getKeys(
const GetPreferencesParameters(
filter: PreferencesFilters(allowList: <String>{stringKey, boolKey}),
),
emptyOptions,
);
expect(keys.length, 2);
expect(keys, contains(stringKey));
expect(keys, contains(boolKey));
});
test('clear', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
await preferences.clear(
const ClearPreferencesParameters(filter: PreferencesFilters()),
emptyOptions);
expect(await preferences.getString(stringKey, emptyOptions), null);
expect(await preferences.getBool(boolKey, emptyOptions), null);
expect(await preferences.getInt(intKey, emptyOptions), null);
expect(await preferences.getDouble(doubleKey, emptyOptions), null);
expect(await preferences.getStringList(listKey, emptyOptions), null);
});
test('clear with filter', () async {
final SharedPreferencesAsyncLinux preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
await preferences.clear(
const ClearPreferencesParameters(
filter: PreferencesFilters(allowList: <String>{stringKey, boolKey}),
),
emptyOptions,
);
expect(await preferences.getString(stringKey, emptyOptions), null);
expect(await preferences.getBool(boolKey, emptyOptions), null);
expect(await preferences.getInt(intKey, emptyOptions), testInt);
expect(await preferences.getDouble(doubleKey, emptyOptions), testDouble);
expect(await preferences.getStringList(listKey, emptyOptions), testList);
});
}
@@ -0,0 +1,66 @@
# Below is a list of people and organizations that have contributed
# to the Flutter project. Names should be added to the list like so:
#
# Name/Organization <email address>
Google Inc.
The Chromium Authors
German Saprykin <saprykin.h@gmail.com>
Benjamin Sauer <sauer.benjamin@gmail.com>
larsenthomasj@gmail.com
Ali Bitek <alibitek@protonmail.ch>
Pol Batlló <pol.batllo@gmail.com>
Anatoly Pulyaevskiy
Hayden Flinner <haydenflinner@gmail.com>
Stefano Rodriguez <hlsroddy@gmail.com>
Salvatore Giordano <salvatoregiordanoo@gmail.com>
Brian Armstrong <brian@flutter.institute>
Paul DeMarco <paulmdemarco@gmail.com>
Fabricio Nogueira <feufeu@gmail.com>
Simon Lightfoot <simon@devangels.london>
Ashton Thomas <ashton@acrinta.com>
Thomas Danner <thmsdnnr@gmail.com>
Diego Velásquez <diego.velasquez.lopez@gmail.com>
Hajime Nakamura <nkmrhj@gmail.com>
Tuyển Vũ Xuân <netsoft1985@gmail.com>
Miguel Ruivo <miguel@miguelruivo.com>
Sarthak Verma <sarthak@artiosys.com>
Mike Diarmid <mike@invertase.io>
Invertase <oss@invertase.io>
Elliot Hesp <elliot@invertase.io>
Vince Varga <vince.varga@smaho.com>
Aawaz Gyawali <awazgyawali@gmail.com>
EUI Limited <ian.evans3@admiralgroup.co.uk>
Katarina Sheremet <katarina@sheremet.ch>
Thomas Stockx <thomas@stockxit.com>
Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
Ozkan Eksi <ozeksi@gmail.com>
Rishab Nayak <rishab@bu.edu>
ko2ic <ko2ic.dev@gmail.com>
Jonathan Younger <jonathan@daikini.com>
Jose Sanchez <josesm82@gmail.com>
Debkanchan Samadder <debu.samadder@gmail.com>
Audrius Karosevicius <audrius.karosevicius@gmail.com>
Lukasz Piliszczuk <lukasz@intheloup.io>
SoundReply Solutions GmbH <ch@soundreply.com>
Rafal Wachol <rwachol@gmail.com>
Pau Picas <pau.picas@gmail.com>
Christian Weder <chrstian.weder@yapeal.ch>
Alexandru Tuca <salexandru.tuca@outlook.com>
Christian Weder <chrstian.weder@yapeal.ch>
Rhodes Davis Jr. <rody.davis.jr@gmail.com>
Luigi Agosti <luigi@tengio.com>
Quentin Le Guennec <quentin@tengio.com>
Koushik Ravikumar <koushik@tengio.com>
Nissim Dsilva <nissim@tengio.com>
Giancarlo Rocha <giancarloiff@gmail.com>
Ryo Miyake <ryo@miyake.id>
Théo Champion <contact.theochampion@gmail.com>
Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
Eitan Schwartz <eshvartz@gmail.com>
Chris Rutkowski <chrisrutkowski89@gmail.com>
Juan Alvarez <juan.alvarez@resideo.com>
Aleksandr Yurkovskiy <sanekyy@gmail.com>
Anton Borries <mail@antonborri.es>
Alex Li <google@alexv525.com>
Rahul Raj <64.rahulraj@gmail.com>
@@ -0,0 +1,128 @@
## 2.4.1 (Plezy vendored patch)
Vendored from pub.dev `shared_preferences_windows` 2.4.1. Upstream writes the whole
preference document with a bare `writeAsStringSync`, which truncates the live
file before writing it, so every preference write has a window in which the
only copy on disk is empty or half-written. A crash inside that window leaves a
store that fails to parse on every later launch — and it holds the
credential-vault key, so the loss is not recoverable by rewriting it. This is
the corruption behind Plezy issue #1732.
The local patch stages the document to a sibling `.tmp`, flushes it, then
renames it over the target, and sweeps a stale staging file once the canonical
document has been read cleanly. The published example app is not vendored.
When updating, reapply both PLEZY DELTA blocks in `lib/shared_preferences_windows.dart`
and see `provenance.json` for the full refresh contract.
## 2.4.1
* Fixes `getStringList` returning immutable list.
* Fixes `getStringList` cast error.
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
## 2.4.0
* Adds `SharedPreferencesAsyncWindows` API.
* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.
## 2.3.2
* Updates `package:file` version constraints.
## 2.3.1
* Adds pub topics to package metadata.
* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
## 2.3.0
* Adds `clearWithParameters` and `getAllWithParameters` methods.
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
## 2.2.0
* Adds `getAllWithPrefix` and `clearWithPrefix` methods.
## 2.1.5
* Clarifies explanation of endorsement in README.
* Aligns Dart and Flutter SDK constraints.
## 2.1.4
* Updates links for the merge of flutter/plugins into flutter/packages.
* Updates minimum Flutter version to 3.0.
## 2.1.3
* Updates code for stricter lint checks.
## 2.1.2
* Updates code for stricter lint checks.
* Updates code for `no_leading_underscores_for_local_identifiers` lint.
* Updates minimum Flutter version to 2.10.
## 2.1.1
* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors
lint warnings.
## 2.1.0
* Deprecated `SharedPreferencesWindows.instance` in favor of `SharedPreferencesStorePlatform.instance`.
## 2.0.4
* Removes dependency on `meta`.
## 2.0.3
* Removed obsolete `pluginClass: none` from pubpsec.
* Fixes newly enabled analyzer options.
## 2.0.2
* Updated installation instructions in README.
## 2.0.1
* Add `implements` to pubspec.yaml.
* Add `registerWith` to the Dart main class.
## 2.0.0
* Migrate to null-safety.
## 0.0.2+3
* Remove 'ffi' dependency.
## 0.0.2+2
* Relax 'ffi' version constraint.
## 0.0.2+1
* Update Flutter SDK constraint.
## 0.0.2
* Update integration test examples to use `testWidgets` instead of `test`.
## 0.0.1+3
* Remove unused `test` dependency.
## 0.0.1+2
* Check in windows/ directory for example/
## 0.0.1+1
* Add iOS stub for compatibility with 1.17 and earlier.
## 0.0.1
* Initial release to support shared_preferences on Windows.
@@ -0,0 +1,25 @@
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,15 @@
# shared\_preferences\_windows
The Windows implementation of [`shared_preferences`][1].
## Usage
This package is [endorsed][2], which means you can simply use `shared_preferences`
normally. This package will be automatically included in your app when you do,
so you do not need to add it to your `pubspec.yaml`.
However, if you `import` this package to use any of its APIs directly, you
should add it to your `pubspec.yaml` as usual.
[1]: https://pub.dev/packages/shared_preferences
[2]: https://flutter.dev/to/endorsed-federated-plugin
@@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}
@@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}
@@ -0,0 +1,456 @@
// 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);
staged.renameSync(localDataFile.path);
} 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();
}
}
@@ -0,0 +1,20 @@
{
"formatVersion": 1,
"upstream": {
"repository": "https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_windows",
"packageVersion": "2.4.1",
"source": "pub.dev"
},
"plezyDeltas": [
"atomic store replacement: stage to a sibling .tmp, flush, then rename over the target",
"stale staging files are swept once the canonical document has been read cleanly",
"the published example app is not vendored"
],
"refreshContract": [
"Diff against the new upstream version and reapply every PLEZY DELTA block in lib/shared_preferences_windows.dart verbatim.",
"Never reintroduce a bare writeAsStringSync onto the live document: it truncates before writing, which is the corruption in issue #1732.",
"Keep the staging name fixed and single. The file is a plaintext copy of the credential-vault key, tracker refresh tokens and Seerr cookies; stamped names would accumulate them.",
"Keep the flush before the rename. A rename can otherwise publish contents that were never committed.",
"Run flutter test test/services/prefs_store_atomic_write_test.dart from the repository root, and the real-Windows step in .github/workflows/ci.yml."
]
}
@@ -0,0 +1,245 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: "direct main"
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_platform_interface:
dependency: "direct main"
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
dependency: "direct main"
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
shared_preferences_platform_interface:
dependency: "direct main"
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
sdks:
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"
@@ -0,0 +1,34 @@
name: shared_preferences_windows
description: Windows implementation of shared_preferences
repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_windows
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22
version: 2.4.1
environment:
sdk: ^3.3.0
flutter: ">=3.19.0"
flutter:
plugin:
implements: shared_preferences
platforms:
windows:
dartPluginClass: SharedPreferencesWindows
dependencies:
file: ">=6.0.0 <8.0.0"
flutter:
sdk: flutter
path: ^1.8.0
path_provider_platform_interface: ^2.0.0
path_provider_windows: ^2.0.0
shared_preferences_platform_interface: ^2.4.0
dev_dependencies:
flutter_test:
sdk: flutter
topics:
- persistence
- shared-preferences
- storage
@@ -0,0 +1,36 @@
// 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 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:path_provider_windows/path_provider_windows.dart';
/// Fake implementation of PathProviderWindows that returns hard-coded paths,
/// allowing tests to run on any platform.
///
/// Note that this should only be used with an in-memory filesystem, as the
/// path it returns is a root path that does not actually exist on Windows.
class FakePathProviderWindows extends PathProviderPlatform
implements PathProviderWindows {
@override
late VersionInfoQuerier versionInfoQuerier;
@override
Future<String?> getApplicationSupportPath() async => r'C:\appsupport';
@override
Future<String?> getTemporaryPath() async => null;
@override
Future<String?> getLibraryPath() async => null;
@override
Future<String?> getApplicationDocumentsPath() async => null;
@override
Future<String?> getDownloadsPath() async => null;
@override
Future<String> getPath(String folderID) async => '';
}
@@ -0,0 +1,258 @@
// 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:convert';
import 'package:file/memory.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider_windows/path_provider_windows.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';
import 'package:shared_preferences_windows/shared_preferences_windows.dart';
import 'fake_path_provider_windows.dart';
void main() {
late MemoryFileSystem fs;
late PathProviderWindows pathProvider;
SharedPreferencesWindows.registerWith();
const Map<String, Object> flutterTestValues = <String, Object>{
'flutter.String': 'hello world',
'flutter.Bool': true,
'flutter.Int': 42,
'flutter.Double': 3.14159,
'flutter.StringList': <String>['foo', 'bar'],
};
const Map<String, Object> prefixTestValues = <String, Object>{
'prefix.String': 'hello world',
'prefix.Bool': true,
'prefix.Int': 42,
'prefix.Double': 3.14159,
'prefix.StringList': <String>['foo', 'bar'],
};
const Map<String, Object> nonPrefixTestValues = <String, Object>{
'String': 'hello world',
'Bool': true,
'Int': 42,
'Double': 3.14159,
'StringList': <String>['foo', 'bar'],
};
final Map<String, Object> allTestValues = <String, Object>{};
allTestValues.addAll(flutterTestValues);
allTestValues.addAll(prefixTestValues);
allTestValues.addAll(nonPrefixTestValues);
setUp(() {
fs = MemoryFileSystem.test();
pathProvider = FakePathProviderWindows();
});
Future<String> getFilePath() async {
final String? directory = await pathProvider.getApplicationSupportPath();
return path.join(directory!, 'shared_preferences.json');
}
Future<void> writeTestFile(String value) async {
fs.file(await getFilePath())
..createSync(recursive: true)
..writeAsStringSync(value);
}
Future<String> readTestFile() async {
return fs.file(await getFilePath()).readAsStringSync();
}
SharedPreferencesWindows getPreferences() {
final SharedPreferencesWindows prefs = SharedPreferencesWindows();
prefs.fs = fs;
prefs.pathProvider = pathProvider;
return prefs;
}
test('registered instance', () async {
SharedPreferencesWindows.registerWith();
expect(SharedPreferencesStorePlatform.instance,
isA<SharedPreferencesWindows>());
});
test('getAll', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesWindows prefs = getPreferences();
final Map<String, Object> values = await prefs.getAll();
expect(values, hasLength(5));
expect(values, flutterTestValues);
});
test('getAllWithPrefix', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesWindows prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithPrefix('prefix.');
expect(values, hasLength(5));
expect(values, prefixTestValues);
});
test('getAllWithParameters with Prefix', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesWindows prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
expect(values, hasLength(5));
expect(values, prefixTestValues);
});
test('getAllWithParameters with Prefix with allow list', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesWindows prefs = getPreferences();
final Map<String?, Object?> all = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(
prefix: 'prefix.',
allowList: <String>{'prefix.Bool'},
),
),
);
expect(all.length, 1);
expect(all['prefix.Bool'], prefixTestValues['prefix.Bool']);
});
test('remove', () async {
await writeTestFile('{"key1":"one","key2":2}');
final SharedPreferencesWindows prefs = getPreferences();
await prefs.remove('key2');
expect(await readTestFile(), '{"key1":"one"}');
});
test('setValue', () async {
await writeTestFile('{}');
final SharedPreferencesWindows prefs = getPreferences();
await prefs.setValue('', 'key1', 'one');
await prefs.setValue('', 'key2', 2);
expect(await readTestFile(), '{"key1":"one","key2":2}');
});
test('clear', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesWindows prefs = getPreferences();
expect(await readTestFile(), json.encode(flutterTestValues));
await prefs.clear();
expect(await readTestFile(), '{}');
});
test('clearWithPrefix', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesWindows prefs = getPreferences();
await prefs.clearWithPrefix('prefix.');
final Map<String, Object> noValues =
await prefs.getAllWithPrefix('prefix.');
expect(noValues, hasLength(0));
final Map<String, Object> values = await prefs.getAll();
expect(values, hasLength(5));
expect(values, flutterTestValues);
});
test('getAllWithNoPrefix', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesWindows prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithPrefix('');
expect(values, hasLength(15));
expect(values, allTestValues);
});
test('clearWithNoPrefix', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesWindows prefs = getPreferences();
await prefs.clearWithPrefix('');
final Map<String, Object> noValues = await prefs.getAllWithPrefix('');
expect(noValues, hasLength(0));
});
test('clearWithParameters with Prefix', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesWindows prefs = getPreferences();
await prefs.clearWithParameters(
ClearParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
final Map<String, Object> noValues = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
expect(noValues, hasLength(0));
final Map<String, Object> values = await prefs.getAll();
expect(values, hasLength(5));
expect(values, flutterTestValues);
});
test('clearWithParameters with allow list', () async {
await writeTestFile(json.encode(prefixTestValues));
final SharedPreferencesWindows prefs = getPreferences();
await prefs.clearWithParameters(
ClearParameters(
filter: PreferencesFilter(
prefix: 'prefix.',
allowList: <String>{'prefix.StringList'},
),
),
);
final Map<String, Object> noValues = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: 'prefix.'),
),
);
expect(noValues, hasLength(4));
});
test('getAllWithNoPrefix', () async {
await writeTestFile(json.encode(allTestValues));
final SharedPreferencesWindows prefs = getPreferences();
final Map<String, Object> values = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: ''),
),
);
expect(values, hasLength(15));
expect(values, allTestValues);
});
test('clearWithNoPrefix', () async {
await writeTestFile(json.encode(flutterTestValues));
final SharedPreferencesWindows prefs = getPreferences();
await prefs.clearWithParameters(
ClearParameters(
filter: PreferencesFilter(prefix: ''),
),
);
final Map<String, Object> noValues = await prefs.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(prefix: ''),
),
);
expect(noValues, hasLength(0));
});
}
@@ -0,0 +1,197 @@
// 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 'package:file/memory.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_windows/path_provider_windows.dart';
import 'package:shared_preferences_platform_interface/types.dart';
import 'package:shared_preferences_windows/shared_preferences_windows.dart';
import 'fake_path_provider_windows.dart';
void main() {
late MemoryFileSystem fs;
late PathProviderWindows pathProvider;
SharedPreferencesAsyncWindows.registerWith();
const String stringKey = 'testString';
const String boolKey = 'testBool';
const String intKey = 'testInt';
const String doubleKey = 'testDouble';
const String listKey = 'testList';
const String testString = 'hello world';
const bool testBool = true;
const int testInt = 42;
const double testDouble = 3.14159;
const List<String> testList = <String>['foo', 'bar'];
const SharedPreferencesWindowsOptions emptyOptions =
SharedPreferencesWindowsOptions();
setUp(() {
fs = MemoryFileSystem.test();
pathProvider = FakePathProviderWindows();
});
SharedPreferencesAsyncWindows getPreferences() {
final SharedPreferencesAsyncWindows prefs = SharedPreferencesAsyncWindows();
prefs.fs = fs;
prefs.pathProvider = pathProvider;
return prefs;
}
test('set and get String', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
expect(await preferences.getString(stringKey, emptyOptions), testString);
});
test('set and get bool', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setBool(boolKey, testBool, emptyOptions);
expect(await preferences.getBool(boolKey, emptyOptions), testBool);
});
test('set and get int', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setInt(intKey, testInt, emptyOptions);
expect(await preferences.getInt(intKey, emptyOptions), testInt);
});
test('set and get double', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
expect(await preferences.getDouble(doubleKey, emptyOptions), testDouble);
});
test('set and get StringList', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setStringList(listKey, testList, emptyOptions);
expect(await preferences.getStringList(listKey, emptyOptions), testList);
});
test('getPreferences', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Map<String, Object?> gotAll = await preferences.getPreferences(
const GetPreferencesParameters(filter: PreferencesFilters()),
emptyOptions);
expect(gotAll.length, 5);
expect(gotAll[stringKey], testString);
expect(gotAll[boolKey], testBool);
expect(gotAll[intKey], testInt);
expect(gotAll[doubleKey], testDouble);
expect(gotAll[listKey], testList);
});
test('getPreferences with filter', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Map<String, Object?> gotAll = await preferences.getPreferences(
const GetPreferencesParameters(
filter:
PreferencesFilters(allowList: <String>{stringKey, boolKey})),
emptyOptions);
expect(gotAll.length, 2);
expect(gotAll[stringKey], testString);
expect(gotAll[boolKey], testBool);
});
test('getKeys', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Set<String> keys = await preferences.getKeys(
const GetPreferencesParameters(filter: PreferencesFilters()),
emptyOptions,
);
expect(keys.length, 5);
expect(keys, contains(stringKey));
expect(keys, contains(boolKey));
expect(keys, contains(intKey));
expect(keys, contains(doubleKey));
expect(keys, contains(listKey));
});
test('getKeys with filter', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
final Set<String> keys = await preferences.getKeys(
const GetPreferencesParameters(
filter: PreferencesFilters(allowList: <String>{stringKey, boolKey}),
),
emptyOptions,
);
expect(keys.length, 2);
expect(keys, contains(stringKey));
expect(keys, contains(boolKey));
});
test('clear', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
await preferences.clear(
const ClearPreferencesParameters(filter: PreferencesFilters()),
emptyOptions);
expect(await preferences.getString(stringKey, emptyOptions), null);
expect(await preferences.getBool(boolKey, emptyOptions), null);
expect(await preferences.getInt(intKey, emptyOptions), null);
expect(await preferences.getDouble(doubleKey, emptyOptions), null);
expect(await preferences.getStringList(listKey, emptyOptions), null);
});
test('clear with filter', () async {
final SharedPreferencesAsyncWindows preferences = getPreferences();
await preferences.setString(stringKey, testString, emptyOptions);
await preferences.setBool(boolKey, testBool, emptyOptions);
await preferences.setInt(intKey, testInt, emptyOptions);
await preferences.setDouble(doubleKey, testDouble, emptyOptions);
await preferences.setStringList(listKey, testList, emptyOptions);
await preferences.clear(
const ClearPreferencesParameters(
filter: PreferencesFilters(allowList: <String>{stringKey, boolKey}),
),
emptyOptions,
);
expect(await preferences.getString(stringKey, emptyOptions), null);
expect(await preferences.getBool(boolKey, emptyOptions), null);
expect(await preferences.getInt(intKey, emptyOptions), testInt);
expect(await preferences.getDouble(doubleKey, emptyOptions), testDouble);
expect(await preferences.getStringList(listKey, emptyOptions), testList);
});
}
+8 -10
View File
@@ -861,7 +861,7 @@ packages:
source: hosted
version: "2.1.3"
path_provider_windows:
dependency: transitive
dependency: "direct dev"
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
@@ -1102,10 +1102,9 @@ packages:
shared_preferences_linux:
dependency: "direct dev"
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
path: "packages/shared_preferences_linux"
relative: true
source: path
version: "2.4.1"
shared_preferences_platform_interface:
dependency: "direct dev"
@@ -1124,12 +1123,11 @@ packages:
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
dependency: "direct dev"
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
path: "packages/shared_preferences_windows"
relative: true
source: path
version: "2.4.1"
shelf:
dependency: transitive
+13 -3
View File
@@ -99,11 +99,14 @@ dev_dependencies:
drift_dev: ^2.28.3
shared_preferences_platform_interface: ^2.4.0
path_provider_platform_interface: ^2.1.0
# Drives the real desktop preference backend against a temp file in tests, so
# the #1732 corrupt-store repair loop is covered on any host and not only on
# a Windows/Linux runner. Both classes expose `fs`/`pathProvider` for this.
# Drive the real desktop preference backends against a temp file in tests, so
# the #1732 corrupt-store repair loop and the vendored atomic write are both
# covered against genuine file I/O. All four expose `fs`/`pathProvider`.
# The two shared_preferences packages resolve to the vendored copies below.
shared_preferences_linux: ^2.4.1
shared_preferences_windows: ^2.4.1
path_provider_linux: ^2.2.1
path_provider_windows: ^2.3.0
plugin_platform_interface: ^2.1.0
freezed: ^3.2.5
analyzer: 10.2.0
@@ -124,6 +127,13 @@ dependency_overrides:
url: https://github.com/edde746/auto_updater
ref: 9e150f7
path: packages/auto_updater_windows
# Vendored 2.4.1 with an atomic store replacement (CHANGELOG, provenance.json)
# — upstream truncates the live preference document on every write, which is
# the corruption behind #1732.
shared_preferences_windows:
path: packages/shared_preferences_windows
shared_preferences_linux:
path: packages/shared_preferences_linux
sentry:
org: plezy
project: plezy
@@ -0,0 +1,135 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider_linux/path_provider_linux.dart';
import 'package:plezy/services/prefs_recovery.dart';
import 'package:shared_preferences_linux/shared_preferences_linux.dart';
import 'package:shared_preferences_platform_interface/types.dart';
/// The vendored `_writePreferences` patch, exercised against a real file
/// system.
///
/// Upstream writes the whole preference document with a bare
/// `writeAsStringSync`, which opens with the default `FileMode.write` and so
/// truncates the live file before writing it. Every preference write therefore
/// has a window in which the only copy on disk is empty or half-written, and
/// the store holds the credential-vault key — the corruption behind #1732.
///
/// `shared_preferences_linux` and `shared_preferences_windows` carry the same
/// patch and are byte-identical apart from their path-provider type, so the
/// Linux copy stands in for both here. Windows rename semantics
/// (`MoveFileExW` with MOVEFILE_REPLACE_EXISTING) cannot be proven on a POSIX
/// host and are covered by the windows-latest step in CI.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Directory support;
late File store;
late File staging;
late SharedPreferencesAsyncLinux backend;
const options = SharedPreferencesOptions();
setUp(() async {
support = await Directory.systemTemp.createTemp('plezy_atomic_write_');
store = File(p.join(support.path, prefsStoreFileName));
staging = File('${store.path}.tmp');
backend = SharedPreferencesAsyncLinux()..pathProvider = _TempPathProviderLinux(support.path);
});
tearDown(() async {
if (await support.exists()) await support.delete(recursive: true);
});
test('a write leaves no staging file behind', () async {
await backend.setString('theme', 'dark', options);
expect(await store.exists(), isTrue);
expect(jsonDecode(await store.readAsString()), containsPair('theme', 'dark'));
expect(await staging.exists(), isFalse);
});
test('a write replaces the document rather than rewriting it in place', () async {
await backend.setString('theme', 'dark', options);
// A hard link names the same inode as the store. Under upstream's
// truncate-then-write the link would observe the new content, because the
// live file is rewritten underneath every reader holding it open — the
// window that makes an interrupted write destroy the document. Under an
// atomic rename the old inode is simply unlinked from the path, so the
// witness keeps exactly what a reader mid-write would still have seen.
final witness = File(p.join(support.path, 'witness.json'));
if (!_run('ln', [store.path, witness.path])) {
// Windows has no `ln` on PATH at all, and NTFS replacement is covered by
// prefs_store_atomic_write_windows_test.dart instead.
markTestSkipped('hard links are unavailable here');
return;
}
await backend.setString('theme', 'light', options);
expect(jsonDecode(await witness.readAsString()), containsPair('theme', 'dark'));
expect(jsonDecode(await store.readAsString()), containsPair('theme', 'light'));
});
test('an interrupted write leaves the live document valid', () async {
await backend.setString('theme', 'dark', options);
final intact = await store.readAsString();
// What an interruption leaves once writes are staged: a half-written
// staging file that was never renamed. The invariant this guards is that
// the staging file is never itself the live document.
await staging.writeAsString('{"theme":"light","trunc', flush: true);
expect(await store.readAsString(), intact);
expect(PrefsRecovery.describeStoreDamage(await store.readAsBytes()), isNull);
});
test('a stale staging file is swept once the document reads cleanly', () async {
await backend.setString('theme', 'dark', options);
await staging.writeAsString('{"credential_vault_key_v1":"left-behind"', flush: true);
// The staging file is a plaintext copy of the credentials, so it must not
// outlive the interrupted write that produced it.
final reader = SharedPreferencesAsyncLinux()..pathProvider = _TempPathProviderLinux(support.path);
await reader.getPreferences(const GetPreferencesParameters(filter: PreferencesFilters()), options);
expect(await staging.exists(), isFalse);
expect(jsonDecode(await store.readAsString()), containsPair('theme', 'dark'));
});
test('writing into a directory that does not exist yet still lands', () async {
final nested = Directory(p.join(support.path, 'nested'));
final nestedBackend = SharedPreferencesAsyncLinux()..pathProvider = _TempPathProviderLinux(nested.path);
await nestedBackend.setString('theme', 'dark', options);
expect(
jsonDecode(await File(p.join(nested.path, prefsStoreFileName)).readAsString()),
containsPair('theme', 'dark'),
);
});
}
class _TempPathProviderLinux extends PathProviderLinux {
_TempPathProviderLinux(this.supportPath);
final String supportPath;
@override
Future<String?> getApplicationSupportPath() async => supportPath;
}
/// Runs a POSIX helper, reporting whether it succeeded.
///
/// Returns false rather than throwing when the binary is missing, so a Windows
/// run reaches `markTestSkipped` instead of failing the whole suite on a
/// ProcessException before the test body can decide anything.
bool _run(String executable, List<String> arguments) {
try {
return Process.runSync(executable, arguments).exitCode == 0;
} on ProcessException {
return false;
}
}
@@ -0,0 +1,99 @@
@TestOn('windows')
library;
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider_windows/path_provider_windows.dart';
import 'package:plezy/services/prefs_recovery.dart';
import 'package:shared_preferences_platform_interface/types.dart';
import 'package:shared_preferences_windows/shared_preferences_windows.dart';
/// The vendored `shared_preferences_windows` atomic write, on real NTFS.
///
/// `prefs_store_atomic_write_test.dart` covers the same patch through the
/// Linux twin and runs everywhere, but it can only prove POSIX `rename(2)`.
/// The store this all exists to protect lives on Windows (#1732), and there
/// the replacement goes through `MoveFileExW` with MOVEFILE_REPLACE_EXISTING —
/// which a POSIX runner and a memory file system are both silent about. Run by
/// the windows-native-test job in CI.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Directory support;
late File store;
late File staging;
late SharedPreferencesAsyncWindows backend;
const options = SharedPreferencesOptions();
setUp(() async {
support = await Directory.systemTemp.createTemp('plezy_atomic_write_win_');
store = File(p.join(support.path, prefsStoreFileName));
staging = File('${store.path}.tmp');
backend = SharedPreferencesAsyncWindows()..pathProvider = _TempPathProviderWindows(support.path);
});
tearDown(() async {
if (await support.exists()) await support.delete(recursive: true);
});
test('rename replaces an existing document on NTFS', () async {
// The bare `MoveFileW` this would otherwise compile to fails outright when
// the destination exists. If the vendored write ever loses its replace
// semantics, every preference write after the first one fails silently —
// `_writePreferences` swallows the exception and returns false.
await backend.setString('theme', 'dark', options);
await backend.setString('theme', 'light', options);
expect(jsonDecode(await store.readAsString()), containsPair('theme', 'light'));
expect(await staging.exists(), isFalse);
});
test('a replaced document is complete and parseable', () async {
for (var i = 0; i < 25; i++) {
await backend.setString('key$i', 'value$i', options);
// Every intermediate state is a whole document, never a truncation.
expect(PrefsRecovery.describeStoreDamage(await store.readAsBytes()), isNull);
}
final document = jsonDecode(await store.readAsString()) as Map<String, dynamic>;
expect(document, containsPair('key0', 'value0'));
expect(document, containsPair('key24', 'value24'));
});
test('a stale staging file is swept once the document reads cleanly', () async {
await backend.setString('theme', 'dark', options);
await staging.writeAsString('{"credential_vault_key_v1":"left-behind"', flush: true);
final reader = SharedPreferencesAsyncWindows()..pathProvider = _TempPathProviderWindows(support.path);
await reader.getPreferences(const GetPreferencesParameters(filter: PreferencesFilters()), options);
expect(await staging.exists(), isFalse);
});
test('an open reader does not block the replacement', () async {
// Windows keeps mandatory locks on open handles, and antivirus and Search
// Indexer both hold the store open. A replacement that a reader can veto
// would turn every preference write into a silent no-op on exactly the
// machines most likely to have damaged the store in the first place.
await backend.setString('theme', 'dark', options);
final handle = await store.open();
addTearDown(handle.close);
await backend.setString('theme', 'light', options);
expect(jsonDecode(await store.readAsString()), containsPair('theme', 'light'));
});
}
class _TempPathProviderWindows extends PathProviderWindows {
_TempPathProviderWindows(this.supportPath);
final String supportPath;
@override
Future<String?> getApplicationSupportPath() async => supportPath;
}