Cut devtools_shared 13.1.0 with new FileSystemExtension API from 14.0.0 (#9895)
diff --git a/packages/devtools_shared/CHANGELOG.md b/packages/devtools_shared/CHANGELOG.md
index eaf9a53..aef5175 100644
--- a/packages/devtools_shared/CHANGELOG.md
+++ b/packages/devtools_shared/CHANGELOG.md
@@ -4,6 +4,26 @@
 found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
 -->
 
+# 13.1.0
+* The `LocalFileSystem` extension APIs are deprecated in favor of a new
+  `FileSystemExtension` extension, which is based on the `FileSystem` class
+  from the `file` package. In detail:
+  * `fileSystem` is a new top-level constant which represents the local (real)
+    file system.
+  * `LocalFileSystem.devToolsDir()` is replaced by a static getter,
+    `FileSystemExtension.devToolsDir`.
+  * `LocalFileSystem.maybeMoveLegacyDevToolsStore()` is replaced by an instance
+    method, `FileSystemExtension.maybeMoveLegacyDevToolsStore()`.
+  * `LocalFileSystem.devToolsStoreLocation()` is replaced by a static getter,
+    `FileSystemExtension.devToolsStoreLocation`.
+  * `LocalFileSystem.ensureDevToolsDirectory()` will be removed.
+  * `LocalFileSystem.devToolsFileFromPath()` is replaced by an instance method,
+    `FileSystemExtension.devToolsFileFromPath()`.
+  * `LocalFileSystem.devToolsFileAsJson()` is replaced by an instance method,
+    `FileSystemExtension.devToolsFileAsJson()`.
+  * `LocalFileSystem.flutterStoreExists()` is replaced by an instance getter,
+    `FileSystemExtension.flutterStoreExists`.
+
 # 13.0.2
 * Validate the `devtoolsOptionsUri` query parameter in the extension enabled
   state handler so it must be a `file:` URI named `devtools_options.yaml`,
diff --git a/packages/devtools_shared/lib/src/server/file_system.dart b/packages/devtools_shared/lib/src/server/file_system.dart
index 6a2036f..620227a 100644
--- a/packages/devtools_shared/lib/src/server/file_system.dart
+++ b/packages/devtools_shared/lib/src/server/file_system.dart
@@ -3,28 +3,37 @@
 // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
 
 import 'dart:convert';
-import 'dart:io';
+import 'dart:io' as io show Platform, File, Directory;
 
+import 'package:file/file.dart' as file;
+import 'package:file/local.dart' as file;
 import 'package:path/path.dart' as path;
 
 import 'devtools_store.dart';
 
+/// The real, local file system, which can be avoided in tests.
+const file.FileSystem fileSystem = file.LocalFileSystem();
+
 /// A namespace local file system utlities.
 extension LocalFileSystem on Never {
   static String _userHomeDir() {
-    final envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
-    return Platform.environment[envKey] ?? '.';
+    final envKey = io.Platform.operatingSystem == 'windows'
+        ? 'APPDATA'
+        : 'HOME';
+    return io.Platform.environment[envKey] ?? '.';
   }
 
   /// Returns the path to the DevTools storage directory.
+  @Deprecated("Replaced by 'FileSystemExtension.devToolsDir'")
   static String devToolsDir() {
     return path.join(_userHomeDir(), '.flutter-devtools');
   }
 
   /// Moves the .devtools file to ~/.flutter-devtools/.devtools if the .devtools
   /// file exists in the user's home directory.
+  @Deprecated("Replaced by 'FileSystemExtension.maybeMoveLegacyDevToolsStore'")
   static void maybeMoveLegacyDevToolsStore() {
-    final file = File(path.join(_userHomeDir(), DevToolsUsage.storeName));
+    final file = io.File(path.join(_userHomeDir(), DevToolsUsage.storeName));
     if (file.existsSync()) {
       ensureDevToolsDirectory();
       file.copySync(devToolsStoreLocation());
@@ -32,19 +41,22 @@
     }
   }
 
+  @Deprecated("Replaced by 'FileSystemExtension.devToolsStoreLocation'")
   static String devToolsStoreLocation() {
     return path.join(devToolsDir(), DevToolsUsage.storeName);
   }
 
   /// Creates the ~/.flutter-devtools directory if it does not already exist.
+  @Deprecated('To be removed')
   static void ensureDevToolsDirectory() {
-    Directory(devToolsDir()).createSync();
+    io.Directory(devToolsDir()).createSync();
   }
 
   /// Returns a DevTools file from the given path.
   ///
   /// Only files within ~/.flutter-devtools/ can be accessed.
-  static File? devToolsFileFromPath(String pathFromDevToolsDir) {
+  @Deprecated("Replaced by 'FileSystemExtension.devToolsFileFromPath'")
+  static io.File? devToolsFileFromPath(String pathFromDevToolsDir) {
     if (pathFromDevToolsDir.contains('..') ||
         path.isAbsolute(pathFromDevToolsDir)) {
       // The passed in path should not be able to walk up the directory tree
@@ -57,7 +69,7 @@
 
     ensureDevToolsDirectory();
     final devToolsDirPath = devToolsDir();
-    final file = File(path.join(devToolsDirPath, pathFromDevToolsDir));
+    final file = io.File(path.join(devToolsDirPath, pathFromDevToolsDir));
     // Defense in depth: ensure the resolved path is actually contained within
     // the DevTools directory.
     if (!path.isWithin(devToolsDirPath, file.path)) {
@@ -72,6 +84,7 @@
   /// Returns a DevTools file from the given path as encoded json.
   ///
   /// Only files within ~/.flutter-devtools/ can be accessed.
+  @Deprecated("Replaced by 'FileSystemExtension.devToolsFileAsJson'")
   static String? devToolsFileAsJson(String pathFromDevToolsDir) {
     final file = devToolsFileFromPath(pathFromDevToolsDir);
     if (file == null) return null;
@@ -86,8 +99,90 @@
   }
 
   /// Whether the flutter store file exists.
+  @Deprecated("Replaced by 'FileSystemExtension.flutterStoreExists'")
   static bool flutterStoreExists() {
-    final flutterStore = File(path.join(_userHomeDir(), '.flutter'));
+    final flutterStore = io.File(path.join(_userHomeDir(), '.flutter'));
+    return flutterStore.existsSync();
+  }
+}
+
+extension FileSystemExtension on file.FileSystem {
+  static String get _userHomeDir {
+    final envKey = io.Platform.operatingSystem == 'windows'
+        ? 'APPDATA'
+        : 'HOME';
+    return io.Platform.environment[envKey] ?? '.';
+  }
+
+  /// The path to the DevTools storage directory.
+  static String get devToolsDir {
+    return path.join(_userHomeDir, '.flutter-devtools');
+  }
+
+  /// Moves the `.devtools` file to `~/.flutter-devtools/.devtools` if the
+  /// `.devtools` file exists in the user's home directory.
+  void maybeMoveLegacyDevToolsStore() {
+    final storeFile = this.file(
+      path.join(_userHomeDir, DevToolsUsage.storeName),
+    );
+    if (storeFile.existsSync()) {
+      _ensureDevToolsDirectory();
+      storeFile.copySync(devToolsStoreLocation);
+      storeFile.deleteSync();
+    }
+  }
+
+  static String get devToolsStoreLocation {
+    return path.join(devToolsDir, DevToolsUsage.storeName);
+  }
+
+  /// Creates the `~/.flutter-devtools` directory if it does not already exist.
+  void _ensureDevToolsDirectory() {
+    directory(devToolsDir).createSync();
+  }
+
+  /// Returns a DevTools file from the given path.
+  ///
+  /// Only files within ~/.flutter-devtools/ can be accessed.
+  file.File? devToolsFileFromPath(String relativePath) {
+    if (relativePath.contains('..') || path.isAbsolute(relativePath)) {
+      // The passed in path should not be able to walk up the directory tree
+      // outside of the `~/.flutter-devtools/` directory. It must also not be an
+      // absolute path: `path.join()` discards the base directory when its
+      // second argument is absolute, which would otherwise allow reading an
+      // arbitrary file on disk (e.g. an absolute path to a credentials `.json`
+      // file).
+      return null;
+    }
+
+    _ensureDevToolsDirectory();
+    final targetFile = this.file(path.join(devToolsDir, relativePath));
+    // Defense in depth: ensure the resolved path is actually contained within
+    // the DevTools directory.
+    if (!path.isWithin(devToolsDir, targetFile.path)) return null;
+    if (!targetFile.existsSync()) return null;
+    return targetFile;
+  }
+
+  /// Returns a DevTools file from the given path as encoded JSON.
+  ///
+  /// Only files within `~/.flutter-devtools/` can be accessed.
+  String? devToolsFileAsJson(String relativePath) {
+    final targetFile = devToolsFileFromPath(relativePath);
+    if (targetFile == null) return null;
+
+    final fileName = path.basename(targetFile.path);
+    if (!fileName.endsWith('.json')) return null;
+
+    final content = targetFile.readAsStringSync();
+    final json = jsonDecode(content) as Map;
+    json['lastModifiedTime'] = targetFile.lastModifiedSync().toString();
+    return jsonEncode(json);
+  }
+
+  /// Whether the flutter store file exists.
+  bool get flutterStoreExists {
+    final flutterStore = this.file(path.join(_userHomeDir, '.flutter'));
     return flutterStore.existsSync();
   }
 }
@@ -96,14 +191,15 @@
   IOPersistentProperties(this.name, {String? documentDirPath}) {
     final fileName = name.replaceAll(' ', '_');
     documentDirPath ??= LocalFileSystem._userHomeDir();
-    _file = File(path.join(documentDirPath, fileName));
+    _file = io.File(path.join(documentDirPath, fileName));
     if (!_file.existsSync()) {
       _file.createSync(recursive: true);
     }
     syncSettings();
   }
 
-  IOPersistentProperties.fromFile(File file) : name = path.basename(file.path) {
+  IOPersistentProperties.fromFile(io.File file)
+    : name = path.basename(file.path) {
     _file = file;
     if (!_file.existsSync()) {
       _file.createSync(recursive: true);
@@ -113,7 +209,7 @@
 
   final String name;
 
-  late File _file;
+  late io.File _file;
 
   late Map<String, Object?> _map;
 
diff --git a/packages/devtools_shared/pubspec.yaml b/packages/devtools_shared/pubspec.yaml
index 332adaa..16cd6c8 100644
--- a/packages/devtools_shared/pubspec.yaml
+++ b/packages/devtools_shared/pubspec.yaml
@@ -4,7 +4,7 @@
 name: devtools_shared
 description: Package of shared Dart structures between devtools_app, dds, and other tools.
 
-version: 13.0.2
+version: 13.1.0
 
 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_shared
 
@@ -18,6 +18,7 @@
   collection: ^1.15.0
   dtd: ^4.0.0
   extension_discovery: ^2.1.0
+  file: ^7.0.0
   meta: ^1.9.1
   path: ^1.8.0
   shelf: ^1.1.0
diff --git a/pubspec.lock b/pubspec.lock
index cc8b58e..1c67b63 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -555,10 +555,10 @@
     dependency: transitive
     description:
       name: meta
-      sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
+      sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
       url: "https://pub.dev"
     source: hosted
-    version: "1.18.3"
+    version: "1.19.0"
   mime:
     dependency: transitive
     description: