|
| 1 | +// Copyright 2020 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +library xdg_directories; |
| 6 | + |
| 7 | +import 'dart:convert'; |
| 8 | +import 'dart:io'; |
| 9 | + |
| 10 | +import 'package:flutter/cupertino.dart'; |
| 11 | +import 'package:path/path.dart' as path; |
| 12 | +import 'package:process/process.dart'; |
| 13 | + |
| 14 | +/// An override function used by the tests to override the environment variable |
| 15 | +/// lookups using [xdgEnvironmentOverride]. |
| 16 | +typedef EnvironmentAccessor = String Function(String envVar); |
| 17 | + |
| 18 | +/// A testing setter that replaces the real environment lookups with an override. |
| 19 | +/// |
| 20 | +/// Set to null to stop overriding. |
| 21 | +/// |
| 22 | +/// Only available to tests. |
| 23 | +@visibleForTesting |
| 24 | +set xdgEnvironmentOverride(EnvironmentAccessor override) { |
| 25 | + _xdgEnvironmentOverride = override; |
| 26 | + _getenv = _xdgEnvironmentOverride ?? _productionGetEnv; |
| 27 | +} |
| 28 | + |
| 29 | +/// A testing getter that returns the current value of the override that |
| 30 | +/// replaces the real environment lookups with an override. |
| 31 | +/// |
| 32 | +/// Only available to tests. |
| 33 | +EnvironmentAccessor get xdgEnvironmentOverride => _xdgEnvironmentOverride; |
| 34 | +EnvironmentAccessor _xdgEnvironmentOverride; |
| 35 | +EnvironmentAccessor _productionGetEnv = |
| 36 | + (String value) => Platform.environment[value]; |
| 37 | +EnvironmentAccessor _getenv = _productionGetEnv; |
| 38 | + |
| 39 | +/// A testing function that replaces the process manager used to run xdg-user-path |
| 40 | +/// with the one supplied. |
| 41 | +/// |
| 42 | +/// Only available to tests. |
| 43 | +@visibleForTesting |
| 44 | +set xdgProcessManager(ProcessManager processManager) { |
| 45 | + _processManager = processManager; |
| 46 | +} |
| 47 | + |
| 48 | +ProcessManager _processManager = const LocalProcessManager(); |
| 49 | + |
| 50 | +List<Directory> _directoryListFromEnvironment( |
| 51 | + String envVar, List<Directory> fallback) { |
| 52 | + assert(envVar != null); |
| 53 | + assert(fallback != null); |
| 54 | + final String value = _getenv(envVar); |
| 55 | + if (value == null || value.isEmpty) { |
| 56 | + return fallback; |
| 57 | + } |
| 58 | + return value.split(':').where((String value) { |
| 59 | + return value.isNotEmpty; |
| 60 | + }).map<Directory>((String entry) { |
| 61 | + return Directory(entry); |
| 62 | + }).toList(); |
| 63 | +} |
| 64 | + |
| 65 | +Directory _directoryFromEnvironment(String envVar, String fallback) { |
| 66 | + assert(envVar != null); |
| 67 | + final String value = _getenv(envVar); |
| 68 | + if (value == null || value.isEmpty) { |
| 69 | + if (fallback == null) { |
| 70 | + return null; |
| 71 | + } |
| 72 | + return _getDirectory(fallback); |
| 73 | + } |
| 74 | + return Directory(value); |
| 75 | +} |
| 76 | + |
| 77 | +// Creates a Directory from a fallback path. |
| 78 | +Directory _getDirectory(String subdir) { |
| 79 | + assert(subdir != null); |
| 80 | + assert(subdir.isNotEmpty); |
| 81 | + final String homeDir = _getenv('HOME'); |
| 82 | + if (homeDir == null || homeDir.isEmpty) { |
| 83 | + throw StateError( |
| 84 | + 'The "HOME" environment variable is not set. This package (and POSIX) ' |
| 85 | + 'requires that HOME be set.'); |
| 86 | + } |
| 87 | + return Directory(path.joinAll(<String>[homeDir, subdir])); |
| 88 | +} |
| 89 | + |
| 90 | +/// The base directory relative to which user-specific |
| 91 | +/// non-essential (cached) data should be written. (Corresponds to |
| 92 | +/// `$XDG_CACHE_HOME`). |
| 93 | +/// |
| 94 | +/// Throws [StateError] if the HOME environment variable is not set. |
| 95 | +Directory get cacheHome => |
| 96 | + _directoryFromEnvironment('XDG_CACHE_HOME', '.cache'); |
| 97 | + |
| 98 | +/// The list of preference-ordered base directories relative to |
| 99 | +/// which configuration files should be searched. (Corresponds to |
| 100 | +/// `$XDG_CONFIG_DIRS`). |
| 101 | +/// |
| 102 | +/// Throws [StateError] if the HOME environment variable is not set. |
| 103 | +List<Directory> get configDirs { |
| 104 | + return _directoryListFromEnvironment( |
| 105 | + 'XDG_CONFIG_DIRS', |
| 106 | + <Directory>[Directory('/etc/xdg')], |
| 107 | + ); |
| 108 | +} |
| 109 | + |
| 110 | +/// The a single base directory relative to which user-specific |
| 111 | +/// configuration files should be written. (Corresponds to `$XDG_CONFIG_HOME`). |
| 112 | +/// |
| 113 | +/// Throws [StateError] if the HOME environment variable is not set. |
| 114 | +Directory get configHome => |
| 115 | + _directoryFromEnvironment('XDG_CONFIG_HOME', '.config'); |
| 116 | + |
| 117 | +/// The list of preference-ordered base directories relative to |
| 118 | +/// which data files should be searched. (Corresponds to `$XDG_DATA_DIRS`). |
| 119 | +/// |
| 120 | +/// Throws [StateError] if the HOME environment variable is not set. |
| 121 | +List<Directory> get dataDirs { |
| 122 | + return _directoryListFromEnvironment( |
| 123 | + 'XDG_DATA_DIRS', |
| 124 | + <Directory>[Directory('/usr/local/share'), Directory('/usr/share')], |
| 125 | + ); |
| 126 | +} |
| 127 | + |
| 128 | +/// The base directory relative to which user-specific data files should be |
| 129 | +/// written. (Corresponds to `$XDG_DATA_HOME`). |
| 130 | +/// |
| 131 | +/// Throws [StateError] if the HOME environment variable is not set. |
| 132 | +Directory get dataHome => |
| 133 | + _directoryFromEnvironment('XDG_DATA_HOME', '.local/share'); |
| 134 | + |
| 135 | +/// The base directory relative to which user-specific runtime |
| 136 | +/// files and other file objects should be placed. (Corresponds to |
| 137 | +/// `$XDG_RUNTIME_DIR`). |
| 138 | +/// |
| 139 | +/// Throws [StateError] if the HOME environment variable is not set. |
| 140 | +Directory get runtimeDir => _directoryFromEnvironment('XDG_RUNTIME_DIR', null); |
| 141 | + |
| 142 | +/// Gets the xdg user directory named by `dirName`. |
| 143 | +/// |
| 144 | +/// Use [getUserDirectoryNames] to find out the list of available names. |
| 145 | +Directory getUserDirectory(String dirName) { |
| 146 | + final ProcessResult result = _processManager.runSync( |
| 147 | + <String>['xdg-user-dir', dirName], |
| 148 | + includeParentEnvironment: true, |
| 149 | + stdoutEncoding: Encoding.getByName('utf8'), |
| 150 | + ); |
| 151 | + final String path = utf8.decode(result.stdout).split('\n')[0]; |
| 152 | + return Directory(path); |
| 153 | +} |
| 154 | + |
| 155 | +/// Gets the set of user directory names that xdg knows about. |
| 156 | +/// |
| 157 | +/// These are not paths, they are names of xdg values. Call [getUserDirectory] |
| 158 | +/// to get the associated directory. |
| 159 | +/// |
| 160 | +/// These are the names of the variables in "[configHome]/user-dirs.dirs", with |
| 161 | +/// the `XDG_` prefix removed and the `_DIR` suffix removed. |
| 162 | +Set<String> getUserDirectoryNames() { |
| 163 | + final File configFile = File(path.join(configHome.path, 'user-dirs.dirs')); |
| 164 | + List<String> contents; |
| 165 | + try { |
| 166 | + contents = configFile.readAsLinesSync(); |
| 167 | + } on FileSystemException { |
| 168 | + return const <String>{}; |
| 169 | + } |
| 170 | + final Set<String> result = <String>{}; |
| 171 | + final RegExp dirRegExp = |
| 172 | + RegExp(r'^\s*XDG_(?<dirname>[^=]*)_DIR\s*=\s*(?<dir>.*)\s*$'); |
| 173 | + for (String line in contents) { |
| 174 | + final RegExpMatch match = dirRegExp.firstMatch(line); |
| 175 | + if (match == null) { |
| 176 | + continue; |
| 177 | + } |
| 178 | + result.add(match.namedGroup('dirname')); |
| 179 | + } |
| 180 | + return result; |
| 181 | +} |
0 commit comments