// 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? _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> _readPreferences() async { _cachedPreferences ??= await _reload( _defaultFileName, fs: fs, pathProvider: pathProvider, ); return _cachedPreferences!; } @override Future clear() async { return clearWithParameters( ClearParameters( filter: PreferencesFilter(prefix: _defaultPrefix), ), ); } @override Future clearWithPrefix(String prefix) async { return clearWithParameters(ClearParameters(filter: PreferencesFilter(prefix: prefix))); } @override Future clearWithParameters(ClearParameters parameters) async { final PreferencesFilter filter = parameters.filter; final Map 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> getAll() async { return getAllWithParameters( GetAllParameters( filter: PreferencesFilter(prefix: _defaultPrefix), ), ); } @override Future> getAllWithPrefix(String prefix) async { return getAllWithParameters(GetAllParameters(filter: PreferencesFilter(prefix: prefix))); } @override Future> getAllWithParameters(GetAllParameters parameters) async { final PreferencesFilter filter = parameters.filter; final Map withPrefix = Map.from(await _readPreferences()); withPrefix .removeWhere((String key, _) => !(key.startsWith(filter.prefix) && (filter.allowList?.contains(key) ?? true))); return withPrefix; } @override Future remove(String key) async { final Map preferences = await _readPreferences(); preferences.remove(key); return _writePreferences( preferences, _defaultFileName, fs: fs, pathProvider: pathProvider, ); } @override Future setValue(String valueType, String key, Object value) async { final Map 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? _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> getKeys( GetPreferencesParameters parameters, SharedPreferencesOptions options, ) async { return (await getPreferences(parameters, options)).keys.toSet(); } @override Future setString( String key, String value, SharedPreferencesOptions options, ) { return _setValue(key, value, options); } @override Future setBool( String key, bool value, SharedPreferencesOptions options, ) { return _setValue(key, value, options); } @override Future setDouble( String key, double value, SharedPreferencesOptions options, ) { return _setValue(key, value, options); } @override Future setInt( String key, int value, SharedPreferencesOptions options, ) { return _setValue(key, value, options); } @override Future setStringList( String key, List value, SharedPreferencesOptions options, ) { return _setValue(key, value, options); } @override Future getString( String key, SharedPreferencesOptions options, ) async { final Map data = await _readAll({key}, options); return data[key] as String?; } @override Future getBool( String key, SharedPreferencesOptions options, ) async { final Map data = await _readAll({key}, options); return data[key] as bool?; } @override Future getDouble( String key, SharedPreferencesOptions options, ) async { final Map data = await _readAll({key}, options); return data[key] as double?; } @override Future getInt( String key, SharedPreferencesOptions options, ) async { final Map data = await _readAll({key}, options); return data[key] as int?; } @override Future?> getStringList( String key, SharedPreferencesOptions options, ) async { final Map data = await _readAll({key}, options); return (data[key] as List?)?.cast().toList(); } @override Future clear(ClearPreferencesParameters parameters, SharedPreferencesOptions options) async { final SharedPreferencesLinuxOptions linuxOptions = SharedPreferencesLinuxOptions.fromSharedPreferencesOptions(options); final PreferencesFilters filter = parameters.filter; final Map 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> getPreferences( GetPreferencesParameters parameters, SharedPreferencesOptions options, ) async { return _readAll(parameters.filter.allowList, options); } /// Reloads preferences from file. @visibleForTesting Future reload( SharedPreferencesLinuxOptions options, ) async { _cachedPreferences = await _reload(options.fileName); } Future> _readAll( Set? allowList, SharedPreferencesOptions options, ) async { final SharedPreferencesLinuxOptions linuxOptions = SharedPreferencesLinuxOptions.fromSharedPreferencesOptions(options); final Map prefs = Map.from(await _readPreferences(linuxOptions.fileName)); prefs.removeWhere((String key, _) => !(allowList?.contains(key) ?? true)); return prefs; } Future _setValue(String key, Object value, SharedPreferencesOptions options) async { final SharedPreferencesLinuxOptions linuxOptions = SharedPreferencesLinuxOptions.fromSharedPreferencesOptions(options); final Map 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> _readPreferences(String fileName) async { _cachedPreferences ??= await _reload( fileName, fs: fs, pathProvider: pathProvider, ); return _cachedPreferences!; } } /// Gets the file where the preferences are stored. Future _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> _reload( String fileName, { FileSystem fs = const LocalFileSystem(), PathProviderLinux? pathProvider, }) async { Map preferences = {}; 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(); } } // 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 _writePreferences( Map 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(); } }