Line data Source code
1 : // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'internal_style.dart';
6 : import 'style.dart';
7 :
8 : class ParsedPath {
9 : /// The [InternalStyle] that was used to parse this path.
10 : InternalStyle style;
11 :
12 : /// The absolute root portion of the path, or `null` if the path is relative.
13 : /// On POSIX systems, this will be `null` or "/". On Windows, it can be
14 : /// `null`, "//" for a UNC path, or something like "C:\" for paths with drive
15 : /// letters.
16 : String? root;
17 :
18 : /// Whether this path is root-relative.
19 : ///
20 : /// See `Context.isRootRelative`.
21 : bool isRootRelative;
22 :
23 : /// The path-separated parts of the path. All but the last will be
24 : /// directories.
25 : List<String> parts;
26 :
27 : /// The path separators preceding each part.
28 : ///
29 : /// The first one will be an empty string unless the root requires a separator
30 : /// between it and the path. The last one will be an empty string unless the
31 : /// path ends with a trailing separator.
32 : List<String> separators;
33 :
34 : /// The file extension of the last non-empty part, or "" if it doesn't have
35 : /// one.
36 0 : String extension([int level = 1]) => _splitExtension(level)[1];
37 :
38 : /// `true` if this is an absolute path.
39 22 : bool get isAbsolute => root != null;
40 :
41 11 : factory ParsedPath.parse(String path, InternalStyle style) {
42 : // Remove the root prefix, if any.
43 11 : final root = style.getRoot(path);
44 11 : final isRootRelative = style.isRootRelative(path);
45 22 : if (root != null) path = path.substring(root.length);
46 :
47 : // Split the parts on path separators.
48 11 : final parts = <String>[];
49 11 : final separators = <String>[];
50 :
51 : var start = 0;
52 :
53 33 : if (path.isNotEmpty && style.isSeparator(path.codeUnitAt(0))) {
54 0 : separators.add(path[0]);
55 : start = 1;
56 : } else {
57 11 : separators.add('');
58 : }
59 :
60 33 : for (var i = start; i < path.length; i++) {
61 22 : if (style.isSeparator(path.codeUnitAt(i))) {
62 22 : parts.add(path.substring(start, i));
63 22 : separators.add(path[i]);
64 11 : start = i + 1;
65 : }
66 : }
67 :
68 : // Add the final part, if any.
69 22 : if (start < path.length) {
70 22 : parts.add(path.substring(start));
71 11 : separators.add('');
72 : }
73 :
74 11 : return ParsedPath._(style, root, isRootRelative, parts, separators);
75 : }
76 :
77 11 : ParsedPath._(
78 : this.style, this.root, this.isRootRelative, this.parts, this.separators);
79 :
80 0 : String get basename {
81 0 : final copy = clone();
82 0 : copy.removeTrailingSeparators();
83 0 : if (copy.parts.isEmpty) return root ?? '';
84 0 : return copy.parts.last;
85 : }
86 :
87 0 : String get basenameWithoutExtension => _splitExtension()[0];
88 :
89 0 : bool get hasTrailingSeparator =>
90 0 : parts.isNotEmpty && (parts.last == '' || separators.last != '');
91 :
92 11 : void removeTrailingSeparators() {
93 55 : while (parts.isNotEmpty && parts.last == '') {
94 0 : parts.removeLast();
95 0 : separators.removeLast();
96 : }
97 77 : if (separators.isNotEmpty) separators[separators.length - 1] = '';
98 : }
99 :
100 11 : void normalize({bool canonicalize = false}) {
101 : // Handle '.', '..', and empty parts.
102 : var leadingDoubles = 0;
103 11 : final newParts = <String>[];
104 22 : for (var part in parts) {
105 22 : if (part == '.' || part == '') {
106 : // Do nothing. Ignore it.
107 11 : } else if (part == '..') {
108 : // Pop the last part off.
109 0 : if (newParts.isNotEmpty) {
110 0 : newParts.removeLast();
111 : } else {
112 : // Backed out past the beginning, so preserve the "..".
113 0 : leadingDoubles++;
114 : }
115 : } else {
116 11 : newParts.add(canonicalize ? style.canonicalizePart(part) : part);
117 : }
118 : }
119 :
120 : // A relative path can back out from the start directory.
121 11 : if (!isAbsolute) {
122 0 : newParts.insertAll(0, List.filled(leadingDoubles, '..'));
123 : }
124 :
125 : // If we collapsed down to nothing, do ".".
126 11 : if (newParts.isEmpty && !isAbsolute) {
127 0 : newParts.add('.');
128 : }
129 :
130 : // Canonicalize separators.
131 11 : parts = newParts;
132 11 : separators =
133 55 : List.filled(newParts.length + 1, style.separator, growable: true);
134 55 : if (!isAbsolute || newParts.isEmpty || !style.needsSeparator(root!)) {
135 22 : separators[0] = '';
136 : }
137 :
138 : // Normalize the Windows root if needed.
139 44 : if (root != null && style == Style.windows) {
140 0 : if (canonicalize) root = root!.toLowerCase();
141 0 : root = root!.replaceAll('/', '\\');
142 : }
143 11 : removeTrailingSeparators();
144 : }
145 :
146 11 : @override
147 : String toString() {
148 11 : final builder = StringBuffer();
149 33 : if (root != null) builder.write(root);
150 44 : for (var i = 0; i < parts.length; i++) {
151 33 : builder.write(separators[i]);
152 33 : builder.write(parts[i]);
153 : }
154 33 : builder.write(separators.last);
155 :
156 11 : return builder.toString();
157 : }
158 :
159 : /// Returns k-th last index of the `character` in the `path`.
160 : ///
161 : /// If `k` exceeds the count of `character`s in `path`, the left most index
162 : /// of the `character` is returned.
163 0 : int _kthLastIndexOf(String path, String character, int k) {
164 : var count = 0, leftMostIndexedCharacter = 0;
165 0 : for (var index = path.length - 1; index >= 0; --index) {
166 0 : if (path[index] == character) {
167 : leftMostIndexedCharacter = index;
168 0 : ++count;
169 0 : if (count == k) {
170 : return index;
171 : }
172 : }
173 : }
174 : return leftMostIndexedCharacter;
175 : }
176 :
177 : /// Splits the last non-empty part of the path into a `[basename, extension]`
178 : /// pair.
179 : ///
180 : /// Takes an optional parameter `level` which makes possible to return
181 : /// multiple extensions having `level` number of dots. If `level` exceeds the
182 : /// number of dots, the path is split at the first most dot. The value of
183 : /// `level` must be greater than 0, else `RangeError` is thrown.
184 : ///
185 : /// Returns a two-element list. The first is the name of the file without any
186 : /// extension. The second is the extension or "" if it has none.
187 0 : List<String> _splitExtension([int level = 1]) {
188 0 : if (level <= 0) {
189 0 : throw RangeError.value(
190 : level, 'level', "level's value must be greater than 0");
191 : }
192 :
193 : final file =
194 0 : parts.cast<String?>().lastWhere((p) => p != '', orElse: () => null);
195 :
196 0 : if (file == null) return ['', ''];
197 0 : if (file == '..') return ['..', ''];
198 :
199 0 : final lastDot = _kthLastIndexOf(file, '.', level);
200 :
201 : // If there is no dot, or it's the first character, like '.bashrc', it
202 : // doesn't count.
203 0 : if (lastDot <= 0) return [file, ''];
204 :
205 0 : return [file.substring(0, lastDot), file.substring(lastDot)];
206 : }
207 :
208 0 : ParsedPath clone() => ParsedPath._(
209 0 : style, root, isRootRelative, List.from(parts), List.from(separators));
210 : }
|