@expo/metro-file-map 56.0.0-0 → 56.0.0-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/build/crawlers/node/fallback.d.ts +7 -0
- package/build/crawlers/node/fallback.js +34 -11
- package/build/crawlers/node/index.js +5 -1
- package/build/lib/TreeFS.js +79 -22
- package/package.json +13 -14
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -6,6 +6,13 @@ type FallbackFilesystemOptions = {
|
|
|
6
6
|
ignore: IgnoreMatcher;
|
|
7
7
|
includeSymlinks: boolean;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Whether a directory node was created or populated by the fallback filesystem.
|
|
11
|
+
* Used by TreeFS to determine if a directory outside the fallback boundary was
|
|
12
|
+
* reached legitimately (e.g., via symlink traversal) and should allow further
|
|
13
|
+
* fallback lookups.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isFallbackDir(dirNode: any): boolean;
|
|
9
16
|
/**
|
|
10
17
|
* Create a FallbackFilesystem that synchronously queries the real filesystem.
|
|
11
18
|
*
|
|
@@ -3,17 +3,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isFallbackDir = isFallbackDir;
|
|
6
7
|
exports.default = createFallbackFilesystem;
|
|
7
8
|
exports.shouldFallbackCrawlDir = shouldFallbackCrawlDir;
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
10
11
|
const normalizePathSeparatorsToPosix_1 = __importDefault(require("../../lib/normalizePathSeparatorsToPosix"));
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const FALLBACK_DIR = Symbol.for('fallbackDir');
|
|
13
|
+
var FallbackFlag;
|
|
14
|
+
(function (FallbackFlag) {
|
|
15
|
+
FallbackFlag[FallbackFlag["VISITED"] = 0] = "VISITED";
|
|
16
|
+
FallbackFlag[FallbackFlag["CRAWLED"] = 1] = "CRAWLED";
|
|
17
|
+
})(FallbackFlag || (FallbackFlag = {}));
|
|
18
|
+
function markDir(dirNode, flag) {
|
|
19
|
+
dirNode[FALLBACK_DIR] = flag;
|
|
14
20
|
}
|
|
15
|
-
function
|
|
16
|
-
return
|
|
21
|
+
function getDirFlag(dirNode) {
|
|
22
|
+
return dirNode[FALLBACK_DIR];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Whether a directory node was created or populated by the fallback filesystem.
|
|
26
|
+
* Used by TreeFS to determine if a directory outside the fallback boundary was
|
|
27
|
+
* reached legitimately (e.g., via symlink traversal) and should allow further
|
|
28
|
+
* fallback lookups.
|
|
29
|
+
*/
|
|
30
|
+
function isFallbackDir(dirNode) {
|
|
31
|
+
return dirNode[FALLBACK_DIR] != null;
|
|
17
32
|
}
|
|
18
33
|
function isDirectory(node) {
|
|
19
34
|
return node instanceof Map;
|
|
@@ -34,7 +49,7 @@ function createFallbackFilesystem(opts) {
|
|
|
34
49
|
return acc;
|
|
35
50
|
}, {});
|
|
36
51
|
function readdir(_normalPath, absolutePath, dirNode) {
|
|
37
|
-
if (dirNode != null &&
|
|
52
|
+
if (dirNode != null && getDirFlag(dirNode) === FallbackFlag.CRAWLED) {
|
|
38
53
|
return dirNode;
|
|
39
54
|
}
|
|
40
55
|
let dirEntries;
|
|
@@ -53,7 +68,9 @@ function createFallbackFilesystem(opts) {
|
|
|
53
68
|
}
|
|
54
69
|
if (entry.isDirectory()) {
|
|
55
70
|
if (!result.has(name)) {
|
|
56
|
-
|
|
71
|
+
const childDir = new Map();
|
|
72
|
+
markDir(childDir, FallbackFlag.VISITED);
|
|
73
|
+
result.set(name, childDir);
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
76
|
else if (entry.isSymbolicLink()) {
|
|
@@ -69,7 +86,7 @@ function createFallbackFilesystem(opts) {
|
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
|
-
markDir(result);
|
|
89
|
+
markDir(result, FallbackFlag.CRAWLED);
|
|
73
90
|
return result;
|
|
74
91
|
}
|
|
75
92
|
return {
|
|
@@ -87,9 +104,15 @@ function createFallbackFilesystem(opts) {
|
|
|
87
104
|
}
|
|
88
105
|
if (stat.isDirectory()) {
|
|
89
106
|
const dirNode = isDirectory(prevNode) ? prevNode : null;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
if (shouldFallbackCrawlDir(absolutePath)) {
|
|
108
|
+
return readdir(normalPath, absolutePath, dirNode);
|
|
109
|
+
}
|
|
110
|
+
if (dirNode != null) {
|
|
111
|
+
return dirNode;
|
|
112
|
+
}
|
|
113
|
+
const newDir = new Map();
|
|
114
|
+
markDir(newDir, FallbackFlag.VISITED);
|
|
115
|
+
return newDir;
|
|
93
116
|
}
|
|
94
117
|
else if (stat.isSymbolicLink()) {
|
|
95
118
|
if (!includeSymlinks) {
|
|
@@ -78,7 +78,11 @@ function find(roots, extensions, ignore, includeSymlinks, rootDir, console, prev
|
|
|
78
78
|
? name
|
|
79
79
|
: dirNormal + path.sep + name;
|
|
80
80
|
if (entry.isDirectory()) {
|
|
81
|
-
|
|
81
|
+
// NOTE(@kitten): We'd like to be able to apply excludes to directories selectively based
|
|
82
|
+
// on their normal paths, so we can exclude using `^...`
|
|
83
|
+
if (!ignore(childNormal)) {
|
|
84
|
+
search(file, childNormal, isWithinRoot || childNormal === '');
|
|
85
|
+
}
|
|
82
86
|
continue;
|
|
83
87
|
}
|
|
84
88
|
const ext = path.extname(file).substr(1);
|
package/build/lib/TreeFS.js
CHANGED
|
@@ -79,6 +79,7 @@ class TreeFS {
|
|
|
79
79
|
#processFile;
|
|
80
80
|
#rootDir;
|
|
81
81
|
#rootPattern;
|
|
82
|
+
#roots;
|
|
82
83
|
#rootNode = new Map();
|
|
83
84
|
constructor(opts) {
|
|
84
85
|
const { rootDir, files, processFile, fallbackFilesystem, roots, serverRoot } = opts;
|
|
@@ -92,13 +93,15 @@ class TreeFS {
|
|
|
92
93
|
else {
|
|
93
94
|
this.#fallbackBoundaryDepth = null;
|
|
94
95
|
}
|
|
96
|
+
const normalRoots = (roots ?? []).map((r) => this.#pathUtils.absoluteToNormal(r));
|
|
97
|
+
this.#roots = normalRoots;
|
|
95
98
|
this.#rootPattern = (0, RootPathUtils_1.pathsToPattern)(roots ?? [], this.#pathUtils);
|
|
96
99
|
if (files != null) {
|
|
97
100
|
this.bulkAddOrModify(files);
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
getSerializableSnapshot() {
|
|
101
|
-
return this.#cloneTree(this.#rootNode
|
|
104
|
+
return this.#cloneTree(this.#rootNode);
|
|
102
105
|
}
|
|
103
106
|
static fromDeserializedSnapshot(args) {
|
|
104
107
|
const { rootDir, fileSystemData, processFile, fallbackFilesystem, roots, serverRoot } = args;
|
|
@@ -428,6 +431,9 @@ class TreeFS {
|
|
|
428
431
|
let targetNormalPath = requestedNormalPath;
|
|
429
432
|
// Lazy-initialised set of seen target paths, to detect symlink cycles.
|
|
430
433
|
let seen;
|
|
434
|
+
// Set when a symlink is followed, to allow fallback population outside
|
|
435
|
+
// the boundary for paths reachable transitively through symlinks.
|
|
436
|
+
let followedSymlink = false;
|
|
431
437
|
// Pointer to the first character of the current path segment in
|
|
432
438
|
// targetNormalPath.
|
|
433
439
|
let fromIdx = opts.start?.pathIdx ?? 0;
|
|
@@ -470,7 +476,7 @@ class TreeFS {
|
|
|
470
476
|
? fromIdx - segmentName.length - 1
|
|
471
477
|
: fromIdx - segmentName.length - 2;
|
|
472
478
|
const parentCanonicalPath = parentEnd > 0 ? targetNormalPath.slice(0, parentEnd) : '';
|
|
473
|
-
segmentNode = this.#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath);
|
|
479
|
+
segmentNode = this.#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath, followedSymlink);
|
|
474
480
|
if (segmentNode != null) {
|
|
475
481
|
ancestorOfRootIdx = null;
|
|
476
482
|
}
|
|
@@ -496,8 +502,7 @@ class TreeFS {
|
|
|
496
502
|
}
|
|
497
503
|
parentNode.set(segmentName, segmentNode);
|
|
498
504
|
}
|
|
499
|
-
else if (!opts.skipFallback &&
|
|
500
|
-
this.#fallbackFilesystem != null) {
|
|
505
|
+
else if (!opts.skipFallback && this.#fallbackFilesystem != null) {
|
|
501
506
|
parentNode.set(segmentName, segmentNode);
|
|
502
507
|
}
|
|
503
508
|
}
|
|
@@ -617,6 +622,7 @@ class TreeFS {
|
|
|
617
622
|
};
|
|
618
623
|
}
|
|
619
624
|
seen.add(targetNormalPath);
|
|
625
|
+
followedSymlink = true;
|
|
620
626
|
fromIdx = 0;
|
|
621
627
|
parentNode = this.#rootNode;
|
|
622
628
|
ancestorOfRootIdx = 0;
|
|
@@ -827,7 +833,7 @@ class TreeFS {
|
|
|
827
833
|
!pathPrefix.endsWith(pathSep + '..')) {
|
|
828
834
|
const canonicalRoot = opts.canonicalPathOfRoot;
|
|
829
835
|
const rootCanonical = pathPrefix === '' ? canonicalRoot : canonicalRoot + path_1.default.sep + pathPrefix;
|
|
830
|
-
this.#populateDirFromFilesystem(iterationRootNode, rootCanonical, false);
|
|
836
|
+
this.#populateDirFromFilesystem(iterationRootNode, rootCanonical, false, false);
|
|
831
837
|
}
|
|
832
838
|
for (const [name, node] of this.#directoryNodeIterator(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx)) {
|
|
833
839
|
if (node == null) {
|
|
@@ -881,7 +887,7 @@ class TreeFS {
|
|
|
881
887
|
const canonicalPath = opts.canonicalPathOfRoot === ''
|
|
882
888
|
? nodePathWithSystemSeparators
|
|
883
889
|
: opts.canonicalPathOfRoot + path_1.default.sep + nodePathWithSystemSeparators;
|
|
884
|
-
this.#populateDirFromFilesystem(node, canonicalPath, false);
|
|
890
|
+
this.#populateDirFromFilesystem(node, canonicalPath, false, false);
|
|
885
891
|
}
|
|
886
892
|
yield* this.#pathIterator(node, iterationRootParentNode, ancestorOfRootIdx != null && ancestorOfRootIdx > 0 ? ancestorOfRootIdx - 1 : null, opts, nodePath, followedLinks);
|
|
887
893
|
}
|
|
@@ -924,25 +930,70 @@ class TreeFS {
|
|
|
924
930
|
}
|
|
925
931
|
return result.node;
|
|
926
932
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
933
|
+
/**
|
|
934
|
+
* Return a filtered view of the tree containing only content under watched
|
|
935
|
+
* roots. Walk each root path from #rootNode, creating intermediate directory
|
|
936
|
+
* nodes as needed, and reference the subtree at each root endpoint directly.
|
|
937
|
+
* Since roots are non-overlapping, each contributes independently.
|
|
938
|
+
*/
|
|
939
|
+
#cloneTree(rootNode) {
|
|
940
|
+
// NOTE(@kitten): The upstream version deeply clones this structure, but this
|
|
941
|
+
// isn't necessary since it's serialized right away by the DiskCacheManager.
|
|
942
|
+
// Even if it isn't, the intention is to store it faithfully, so we're okay
|
|
943
|
+
// with more modifications
|
|
944
|
+
function copyRootInto(normalRoot, source, clone) {
|
|
945
|
+
let currentSource = source;
|
|
946
|
+
let currentClone = clone;
|
|
947
|
+
let fromIdx = 0;
|
|
948
|
+
while (fromIdx < normalRoot.length) {
|
|
949
|
+
const nextSepIdx = normalRoot.indexOf(path_1.default.sep, fromIdx);
|
|
950
|
+
const isLastSegment = nextSepIdx === -1;
|
|
951
|
+
const seg = isLastSegment
|
|
952
|
+
? normalRoot.slice(fromIdx)
|
|
953
|
+
: normalRoot.slice(fromIdx, nextSepIdx);
|
|
954
|
+
fromIdx = isLastSegment ? normalRoot.length : nextSepIdx + 1;
|
|
955
|
+
const sourceChild = currentSource.get(seg);
|
|
956
|
+
if (sourceChild == null || !isDirectory(sourceChild)) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
else if (isLastSegment || fromIdx >= normalRoot.length) {
|
|
960
|
+
currentClone.set(seg, sourceChild);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
let cloneChild = currentClone.get(seg);
|
|
964
|
+
if (cloneChild == null || !isDirectory(cloneChild)) {
|
|
965
|
+
cloneChild = new Map();
|
|
966
|
+
currentClone.set(seg, cloneChild);
|
|
967
|
+
}
|
|
968
|
+
currentSource = sourceChild;
|
|
969
|
+
currentClone = cloneChild;
|
|
970
|
+
}
|
|
932
971
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
972
|
+
}
|
|
973
|
+
if (this.#roots.length === 0) {
|
|
974
|
+
return rootNode;
|
|
975
|
+
}
|
|
976
|
+
const clone = new Map();
|
|
977
|
+
for (const normalRoot of this.#roots) {
|
|
978
|
+
if (normalRoot === '') {
|
|
979
|
+
// Root is rootDir itself — include everything except '..'
|
|
980
|
+
for (const [name, node] of rootNode) {
|
|
981
|
+
if (node != null && name !== '..') {
|
|
982
|
+
clone.set(name, node);
|
|
983
|
+
}
|
|
937
984
|
}
|
|
938
985
|
}
|
|
939
986
|
else {
|
|
940
|
-
|
|
987
|
+
copyRootInto(normalRoot, rootNode, clone);
|
|
941
988
|
}
|
|
942
989
|
}
|
|
943
990
|
return clone;
|
|
944
991
|
}
|
|
945
|
-
#isOutsideFallbackBoundary(canonicalPath) {
|
|
992
|
+
#isOutsideFallbackBoundary(canonicalPath, dirNode) {
|
|
993
|
+
// We allow any directory that's already been crawled
|
|
994
|
+
if ((0, fallback_1.isFallbackDir)(dirNode)) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
946
997
|
const maxDepth = this.#fallbackBoundaryDepth;
|
|
947
998
|
return maxDepth != null && (0, RootPathUtils_1.getAncestorOfRootIdx)(canonicalPath) > maxDepth;
|
|
948
999
|
}
|
|
@@ -951,20 +1002,26 @@ class TreeFS {
|
|
|
951
1002
|
* fallback filesystem. The fallback returns tree-compatible nodes
|
|
952
1003
|
* (FileMetadata tuples or directory Maps) that are inserted directly.
|
|
953
1004
|
*
|
|
1005
|
+
* Accepts `wasFollowing` to allow traversal on symlinks that were followed.
|
|
1006
|
+
* If we're resolving a symlink target, we allow the lookup to escape the
|
|
1007
|
+
* fallback scope.
|
|
1008
|
+
*
|
|
954
1009
|
* Returns the newly created node, or null if the path doesn't exist on disk.
|
|
955
1010
|
*/
|
|
956
|
-
#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath) {
|
|
1011
|
+
#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath, wasFollowing) {
|
|
957
1012
|
const fallback = this.#fallbackFilesystem;
|
|
958
1013
|
if (fallback == null) {
|
|
959
1014
|
return null;
|
|
960
1015
|
}
|
|
1016
|
+
// A symlink traversal (wasFollowing) or a parent created by the fallback
|
|
1017
|
+
// (isFallbackDir) lets us skip the boundary check.
|
|
961
1018
|
const childCanonicalPath = parentCanonicalPath === '' ? segmentName : parentCanonicalPath + path_1.default.sep + segmentName;
|
|
962
1019
|
if (this.#rootPattern?.test(childCanonicalPath + path_1.default.sep) ||
|
|
963
|
-
this.#isOutsideFallbackBoundary(childCanonicalPath)) {
|
|
1020
|
+
(!wasFollowing && this.#isOutsideFallbackBoundary(childCanonicalPath, parentNode))) {
|
|
964
1021
|
return null;
|
|
965
1022
|
}
|
|
966
1023
|
else if (parentCanonicalPath !== '' && (0, fallback_1.shouldFallbackCrawlDir)(parentCanonicalPath)) {
|
|
967
|
-
this.#populateDirFromFilesystem(parentNode, parentCanonicalPath, true);
|
|
1024
|
+
this.#populateDirFromFilesystem(parentNode, parentCanonicalPath, true, wasFollowing);
|
|
968
1025
|
return parentNode.get(segmentName) ?? null;
|
|
969
1026
|
}
|
|
970
1027
|
else if (parentNode.has(segmentName)) {
|
|
@@ -984,12 +1041,12 @@ class TreeFS {
|
|
|
984
1041
|
* iteration, and by #populateFromFilesystem for optimistic parent
|
|
985
1042
|
* population.
|
|
986
1043
|
*/
|
|
987
|
-
#populateDirFromFilesystem(dirNode, canonicalPath, skipCheck) {
|
|
1044
|
+
#populateDirFromFilesystem(dirNode, canonicalPath, skipCheck, wasFollowed) {
|
|
988
1045
|
const fallback = this.#fallbackFilesystem;
|
|
989
1046
|
if (fallback == null ||
|
|
990
1047
|
(!skipCheck &&
|
|
991
1048
|
(this.#rootPattern?.test(canonicalPath + path_1.default.sep) ||
|
|
992
|
-
this.#isOutsideFallbackBoundary(canonicalPath)))) {
|
|
1049
|
+
(!wasFollowed && this.#isOutsideFallbackBoundary(canonicalPath, dirNode))))) {
|
|
993
1050
|
return;
|
|
994
1051
|
}
|
|
995
1052
|
const absolutePath = this.#pathUtils.normalToAbsolute(canonicalPath);
|
package/package.json
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/metro-file-map",
|
|
3
|
-
"version": "56.0.0-
|
|
3
|
+
"version": "56.0.0-2",
|
|
4
4
|
"description": "A metro-file-map fork for Expo used with the Metro bundler",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "expo-module tsc",
|
|
9
|
-
"clean": "expo-module clean",
|
|
10
|
-
"lint": "expo-module lint",
|
|
11
|
-
"prepublishOnly": "pnpm run clean && pnpm run build",
|
|
12
|
-
"test": "expo-module test",
|
|
13
|
-
"typecheck": "expo-module typecheck",
|
|
14
|
-
"watch": "expo-module tsc --watch --preserveWatchOutput"
|
|
15
|
-
},
|
|
16
7
|
"repository": {
|
|
17
8
|
"type": "git",
|
|
18
9
|
"url": "https://github.com/expo/expo.git",
|
|
@@ -45,11 +36,19 @@
|
|
|
45
36
|
"@types/invariant": "^2.2.37",
|
|
46
37
|
"@types/micromatch": "^4.0.10",
|
|
47
38
|
"@types/node": "^22.14.0",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
39
|
+
"memfs": "^3.6.0",
|
|
40
|
+
"expo-module-scripts": "56.0.2"
|
|
50
41
|
},
|
|
51
42
|
"publishConfig": {
|
|
52
43
|
"access": "public"
|
|
53
44
|
},
|
|
54
|
-
"gitHead": "
|
|
55
|
-
|
|
45
|
+
"gitHead": "a8ab3da510a34b7bdb2262aa9887d4f78b102280",
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "expo-module tsc",
|
|
48
|
+
"clean": "expo-module clean",
|
|
49
|
+
"lint": "expo-module lint",
|
|
50
|
+
"test": "expo-module test",
|
|
51
|
+
"typecheck": "expo-module typecheck",
|
|
52
|
+
"watch": "expo-module tsc --watch --preserveWatchOutput"
|
|
53
|
+
}
|
|
54
|
+
}
|