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:
@@ -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: |
|
||||
|
||||
@@ -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.
|
||||
@@ -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":{}}
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
[]
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"format-version":[1,0,0],"native-assets":{}}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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));
|
||||
});
|
||||
}
|
||||
+203
@@ -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":{}}
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
[]
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"format-version":[1,0,0],"native-assets":{}}
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -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));
|
||||
});
|
||||
}
|
||||
+197
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user