fix(mongo): ignore comments in shell JSON arguments

This commit is contained in:
James Leong authored and GitHub committed 2026-07-22 15:10:40 +08:00
1 parent 94a40a8025
commit 63f375a7d8
3 files changed
+175 -30

No files matched your search

+86 -18
View File
@@ -7,9 +7,11 @@
export function normalizeJsonArgument(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return "{}";
const withoutComments = stripMongoJsonComments(trimmed).trim();
if (!withoutComments) return "{}";
// Rewrite mongo shell constructors that are not valid JSON into extended JSON
// (mongo_driver::json_value_to_bson): ObjectId / NumberLong / ISODate / new Date.
const withExtendedJson = replaceMongoShellConstructors(trimmed);
const withExtendedJson = replaceMongoShellConstructors(withoutComments);
const preprocessed = quoteUnquotedObjectKeys(convertSingleQuotedStrings(withExtendedJson));
try {
JSON.parse(preprocessed);
@@ -32,18 +34,13 @@ export function parseMongoObjectArgument(arg: string | undefined): string | null
}
}
export function parseCollectionMethodTarget(
source: string,
method: string,
): { collection: string; methodCallIndex: number } | null {
export function parseCollectionMethodTarget(source: string, method: string): { collection: string; methodCallIndex: number } | null {
const escapedMethod = escapeRegExp(method);
const direct = new RegExp(`^db\\s*\\.\\s*([A-Za-z_$][\\w$]*)\\s*\\.\\s*${escapedMethod}\\s*\\(`).exec(source);
if (direct) {
return { collection: direct[1]!, methodCallIndex: findChainedMethodCallIndex(source, method) };
}
const getCollection = new RegExp(
`^db\\s*\\.\\s*getCollection\\s*\\(\\s*(["'])(.*?)\\1\\s*\\)\\s*\\.\\s*${escapedMethod}\\s*\\(`,
).exec(source);
const getCollection = new RegExp(`^db\\s*\\.\\s*getCollection\\s*\\(\\s*(["'])(.*?)\\1\\s*\\)\\s*\\.\\s*${escapedMethod}\\s*\\(`).exec(source);
if (getCollection) {
return { collection: getCollection[2]!, methodCallIndex: findChainedMethodCallIndex(source, method) };
}
@@ -78,6 +75,12 @@ export function splitTopLevel(source: string): string[] {
continue;
}
const commentEnd = mongoCommentEndAt(source, i);
if (commentEnd !== null) {
i = commentEnd - 1;
continue;
}
if (char === '"' || char === "'") quote = char;
else if (char === "{" || char === "[" || char === "(") depth += 1;
else if (char === "}" || char === "]" || char === ")") depth -= 1;
@@ -106,6 +109,12 @@ export function findMatchingParen(source: string, openIndex: number): number {
continue;
}
const commentEnd = mongoCommentEndAt(source, i);
if (commentEnd !== null) {
i = commentEnd - 1;
continue;
}
if (char === '"' || char === "'") quote = char;
else if (char === "(") depth += 1;
else if (char === ")") {
@@ -130,6 +139,11 @@ export function hasUnclosedMongoDelimiters(source: string): boolean {
else if (char === quote) quote = null;
continue;
}
const commentEnd = mongoCommentEndAt(source, i);
if (commentEnd !== null) {
i = commentEnd - 1;
continue;
}
if (char === '"' || char === "'") {
quote = char;
continue;
@@ -146,14 +160,52 @@ export function hasUnclosedMongoDelimiters(source: string): boolean {
return quote !== null || stack.length > 0;
}
/** Remove shell/SQL-style comments from JSON-like Mongo arguments. */
export function stripMongoJsonComments(source: string): string {
let result = "";
let quote: string | null = null;
let escaped = false;
for (let i = 0; i < source.length; i += 1) {
const char = source[i] ?? "";
if (quote) {
result += char;
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === quote) quote = null;
continue;
}
if (char === '"' || char === "'") {
quote = char;
result += char;
continue;
}
const commentEnd = mongoCommentEndAt(source, i);
if (commentEnd !== null) {
result += source
.slice(i, commentEnd)
.replace(/[^\n\r\u2028\u2029]/g, " ")
.replace(/[\u2028\u2029]/g, "\n");
i = commentEnd - 1;
continue;
}
result += char;
}
return result;
}
/** Strip leading line/block comments (//, --, and block comments). */
export function trimMongoOuterComments(source: string): string {
let text = source;
for (;;) {
const trimmed = text.trimStart();
if (trimmed.startsWith("//") || trimmed.startsWith("--")) {
const nl = trimmed.indexOf("\n");
text = nl < 0 ? "" : trimmed.slice(nl + 1);
const end = mongoLineCommentEnd(trimmed, 2);
text = end >= trimmed.length ? "" : trimmed.slice(end);
continue;
}
if (trimmed.startsWith("/*")) {
@@ -166,6 +218,28 @@ export function trimMongoOuterComments(source: string): string {
}
}
function mongoCommentEndAt(source: string, index: number): number | null {
const current = source[index];
const next = source[index + 1];
if ((current === "/" && next === "/") || (current === "-" && next === "-")) {
return mongoLineCommentEnd(source, index + 2);
}
if (current === "/" && next === "*") {
const end = source.indexOf("*/", index + 2);
return end < 0 ? source.length : end + 2;
}
return null;
}
function mongoLineCommentEnd(source: string, start: number): number {
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (char === "\r") return source[index + 1] === "\n" ? index + 2 : index + 1;
if (char === "\n" || char === "\u2028" || char === "\u2029") return index + 1;
}
return source.length;
}
export function quoteUnquotedObjectKeys(source: string): string {
let result = "";
let quote: string | null = null;
@@ -213,8 +287,7 @@ function shouldQuoteObjectKey(source: string, index: number): boolean {
}
function replaceMongoShellConstructors(source: string): string {
const constructor =
/^(ObjectId|NumberLong|ISODate)\s*\(\s*["']([^"']+)["']\s*\)|^(ObjectId|NumberLong)\s*\(\s*(-?\d+)\s*\)|^(?:new\s+Date)\s*\(\s*["']([^"']+)["']\s*\)/;
const constructor = /^(ObjectId|NumberLong|ISODate)\s*\(\s*["']([^"']+)["']\s*\)|^(ObjectId|NumberLong)\s*\(\s*(-?\d+)\s*\)|^(?:new\s+Date)\s*\(\s*["']([^"']+)["']\s*\)/;
let result = "";
let index = 0;
while (index < source.length) {
@@ -237,12 +310,7 @@ function replaceMongoShellConstructors(source: string): string {
continue;
}
if (match[1]) {
result +=
match[1] === "ObjectId"
? `{"$oid":"${match[2]}"}`
: match[1] === "NumberLong"
? `{"$numberLong":"${match[2]}"}`
: `{"$date":"${match[2]}"}`;
result += match[1] === "ObjectId" ? `{"$oid":"${match[2]}"}` : match[1] === "NumberLong" ? `{"$numberLong":"${match[2]}"}` : `{"$date":"${match[2]}"}`;
} else if (match[3]) {
result += match[3] === "NumberLong" ? `{"$numberLong":"${match[4]}"}` : `{"$oid":"${match[4]}"}`;
} else {