Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d6b84c7

Browse files
fix: remove dependency on export tool input for generating export name MCP-189 (#543)
1 parent ed8c19f commit d6b84c7

File tree

5 files changed

+18
-44
lines changed

5 files changed

+18
-44
lines changed

‎src/common/exportsManager.ts‎

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
127127
public async readExport(exportName: string): Promise<string> {
128128
try {
129129
this.assertIsNotShuttingDown();
130-
exportName = decodeURIComponent(exportName);
130+
exportName = decodeAndNormalize(exportName);
131131
const exportHandle = this.storedExports[exportName];
132132
if (!exportHandle) {
133133
throw new Error("Requested export has either expired or does not exist.");
@@ -163,7 +163,7 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
163163
}): Promise<AvailableExport> {
164164
try {
165165
this.assertIsNotShuttingDown();
166-
const exportNameWithExtension = validateExportName(ensureExtension(exportName, "json"));
166+
const exportNameWithExtension = decodeAndNormalize(ensureExtension(exportName, "json"));
167167
if (this.storedExports[exportNameWithExtension]) {
168168
return Promise.reject(
169169
new Error("Export with same name is either already available or being generated.")
@@ -363,6 +363,10 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
363363
}
364364
}
365365

366+
export function decodeAndNormalize(text: string): string {
367+
return decodeURIComponent(text).normalize("NFKC");
368+
}
369+
366370
/**
367371
* Ensures the path ends with the provided extension */
368372
export function ensureExtension(pathOrName: string, extension: string): string {
@@ -373,22 +377,6 @@ export function ensureExtension(pathOrName: string, extension: string): string {
373377
return `${pathOrName}${extWithDot}`;
374378
}
375379

376-
/**
377-
* Small utility to decoding and validating provided export name for path
378-
* traversal or no extension */
379-
export function validateExportName(nameWithExtension: string): string {
380-
const decodedName = decodeURIComponent(nameWithExtension);
381-
if (!path.extname(decodedName)) {
382-
throw new Error("Provided export name has no extension");
383-
}
384-
385-
if (decodedName.includes("..") || decodedName.includes("/") || decodedName.includes("\\")) {
386-
throw new Error("Invalid export name: path traversal hinted");
387-
}
388-
389-
return decodedName;
390-
}
391-
392380
export function isExportExpired(createdAt: number, exportTimeoutMs: number): boolean {
393381
return Date.now() - createdAt > exportTimeoutMs;
394382
}

‎src/resources/common/exportedData.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ export class ExportedData {
7272
private autoCompleteExportName: CompleteResourceTemplateCallback = (value) => {
7373
try {
7474
return this.session.exportsManager.availableExports
75-
.filter(({ exportName }) => exportName.startsWith(value))
75+
.filter(({ exportName, exportTitle }) => {
76+
const lcExportName = exportName.toLowerCase();
77+
const lcExportTitle = exportTitle.toLowerCase();
78+
const lcValue = value.toLowerCase();
79+
return lcExportName.startsWith(lcValue) || lcExportTitle.includes(lcValue);
80+
})
7681
.map(({ exportName }) => exportName);
7782
} catch (error) {
7883
this.session.logger.error({

‎src/tools/mongodb/read/export.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class ExportTool extends MongoDBToolBase {
8181
});
8282
}
8383

84-
const exportName = `${database}.${collection}.${new ObjectId().toString()}.json`;
84+
const exportName = `${new ObjectId().toString()}.json`;
8585

8686
const { exportURI, exportPath } = await this.session.exportsManager.createJSONExport({
8787
input: cursor,

‎tests/integration/resources/exportedData.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ describeWithMongoDB(
155155
},
156156
argument: {
157157
name: "exportName",
158-
value: "b",
158+
value: "big",
159159
},
160160
});
161-
expect(completeResponse.completion.total).toEqual(1);
161+
expect(completeResponse.completion.total).toBeGreaterThanOrEqual(1);
162162
});
163163
});
164164
},

‎tests/unit/common/exportsManager.test.ts‎

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import type { FindCursor } from "mongodb";
55
import { Long } from "mongodb";
66
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
77
import type { ExportsManagerConfig } from "../../../src/common/exportsManager.js";
8-
import {
9-
ensureExtension,
10-
isExportExpired,
11-
ExportsManager,
12-
validateExportName,
13-
} from "../../../src/common/exportsManager.js";
8+
import { ensureExtension, isExportExpired, ExportsManager } from "../../../src/common/exportsManager.js";
149
import type { AvailableExport } from "../../../src/common/exportsManager.js";
1510
import { config } from "../../../src/common/config.js";
1611
import { ROOT_DIR } from "../../accuracy/sdk/constants.js";
@@ -30,14 +25,10 @@ const exportsManagerConfig: ExportsManagerConfig = {
3025
function getExportNameAndPath({
3126
uniqueExportsId = new ObjectId().toString(),
3227
uniqueFileId = new ObjectId().toString(),
33-
database = "foo",
34-
collection = "bar",
3528
}:
3629
| {
3730
uniqueExportsId?: string;
3831
uniqueFileId?: string;
39-
database?: string;
40-
collection?: string;
4132
}
4233
| undefined = {}): {
4334
sessionExportsPath: string;
@@ -46,7 +37,7 @@ function getExportNameAndPath({
4637
exportURI: string;
4738
uniqueExportsId: string;
4839
} {
49-
const exportName = `${database}.${collection}.${uniqueFileId}.json`;
40+
const exportName = `${uniqueFileId}.json`;
5041
// This is the exports directory for a session.
5142
const sessionExportsPath = path.join(exportsPath, uniqueExportsId);
5243
const exportPath = path.join(sessionExportsPath, exportName);
@@ -248,7 +239,7 @@ describe("ExportsManager unit test", () => {
248239
});
249240

250241
it("should handle encoded name", async () => {
251-
const { exportName, exportURI } = getExportNameAndPath({ database: "some database",collection: "coll" });
242+
const { exportName, exportURI } = getExportNameAndPath({ uniqueFileId: "1FOO 2BAR" });
252243
const { cursor } = createDummyFindCursor([]);
253244
const exportAvailableNotifier = getExportAvailableNotifier(encodeURI(exportURI), manager);
254245
await manager.createJSONExport({
@@ -611,16 +602,6 @@ describe("#ensureExtension", () => {
611602
});
612603
});
613604

614-
describe("#validateExportName", () => {
615-
it("should return decoded name when name is valid", () => {
616-
expect(validateExportName(encodeURIComponent("Test Name.json"))).toEqual("Test Name.json");
617-
});
618-
it("should throw when name is invalid", () => {
619-
expect(() => validateExportName("NoExtension")).toThrow("Provided export name has no extension");
620-
expect(() => validateExportName("../something.json")).toThrow("Invalid export name: path traversal hinted");
621-
});
622-
});
623-
624605
describe("#isExportExpired", () => {
625606
it("should return true if export is expired", () => {
626607
const createdAt = Date.now() - 1000;

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /