Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle special characters in filters #1016

Merged
merged 9 commits into from
Mar 22, 2024
2 changes: 1 addition & 1 deletion benchexec/tablegenerator/react-table/build/main.min.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,11 @@ export default class Overview extends React.Component {
filters
.filter((filter) => filter.id !== "id")
.forEach((filter) => {
const [runsetIdx, , columnIdx] = filter.id.split("_");
const type = this.state.tools[runsetIdx]["columns"][columnIdx].type;
const filterSplitArray = filter.id.split("_");
const type =
this.state.tools[filterSplitArray[0]]["columns"][
filterSplitArray.at(-1)
].type;
filter.type = type;
});

Expand Down
149 changes: 125 additions & 24 deletions benchexec/tablegenerator/react-table/src/tests/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
makeFilterDeserializer,
splitUrlPathForMatchingPrefix,
makeRegExp,
tokenizePart,
} from "../utils/utils";

describe("isStatusOk", () => {
Expand Down Expand Up @@ -134,54 +135,71 @@ describe("hashRouting helpers", () => {
const baseUrl = "http://example.com";
const params = { key1: "value1", key2: "value2" };

expect(constructHashURL(baseUrl, params)).toEqual(
"http://example.com?key1=value1&key2=value2",
);
const expected = {
newUrl: "http://example.com?key1=value1&key2=value2",
queryString: "?key1=value1&key2=value2",
};
expect(constructHashURL(baseUrl, params)).toEqual(expected);
});

test("should construct URL hash with provided parameters and keep the exisiting parameters", () => {
const baseUrl = "http://example.com?existingKey=existingValue";
const params = { key1: "value1", key2: "value2" };

expect(constructHashURL(baseUrl, params)).toEqual(
"http://example.com?existingKey=existingValue&key1=value1&key2=value2",
);
const expected = {
newUrl:
"http://example.com?existingKey=existingValue&key1=value1&key2=value2",
queryString: "?existingKey=existingValue&key1=value1&key2=value2",
};
expect(constructHashURL(baseUrl, params)).toEqual(expected);
});

test("should return the same URL with exisiting params if no parameters are provided", () => {
const baseUrl = "http://example.com?exisitingKey=existingValue";
const params = {};

expect(constructHashURL(baseUrl, params)).toEqual(
"http://example.com?exisitingKey=existingValue",
);
const expected = {
newUrl: "http://example.com?exisitingKey=existingValue",
queryString: "?exisitingKey=existingValue",
};

expect(constructHashURL(baseUrl, params)).toEqual(expected);
});

test("should override existing parameters with new ones", () => {
const baseUrl = "http://example.com?key1=value1&key2=value2";
const params = { key2: "newValue" };

expect(constructHashURL(baseUrl, params)).toEqual(
"http://example.com?key1=value1&key2=newValue",
);
const expected = {
newUrl: "http://example.com?key1=value1&key2=newValue",
queryString: "?key1=value1&key2=newValue",
};

expect(constructHashURL(baseUrl, params)).toEqual(expected);
});

test("should remove exisiting parameters if they are updated to undefined", () => {
const baseUrl = "http://example.com?key1=value1&key2=value2";
const params = { key2: undefined };

expect(constructHashURL(baseUrl, params)).toEqual(
"http://example.com?key1=value1",
);
const expected = {
newUrl: "http://example.com?key1=value1",
queryString: "?key1=value1",
};

expect(constructHashURL(baseUrl, params)).toEqual(expected);
});

test("should remove exisiting parameters if they are updated to null", () => {
const baseUrl = "http://example.com?key1=value1&key2=value2";
const params = { key2: null };

expect(constructHashURL(baseUrl, params)).toEqual(
"http://example.com?key1=value1",
);
const expected = {
newUrl: "http://example.com?key1=value1",
queryString: "?key1=value1",
};

expect(constructHashURL(baseUrl, params)).toEqual(expected);
});

test("should not remove exisiting parameters if they are updated to falsy values", () => {
Expand All @@ -192,9 +210,12 @@ describe("hashRouting helpers", () => {
key3: 0,
};

expect(constructHashURL(baseUrl, params)).toEqual(
"http://example.com?key1=&key2=false&key3=0",
);
const expected = {
newUrl: "http://example.com?key1=&key2=false&key3=0",
queryString: "?key1=&key2=false&key3=0",
};

expect(constructHashURL(baseUrl, params)).toEqual(expected);
});
});
});
Expand Down Expand Up @@ -239,9 +260,29 @@ describe("decodeFilter", () => {
expect(decodeFilter(filter)).toEqual(expected);
});

test("should throw errors if there are more than two '_' in the filter id", () => {
expect(() => decodeFilter("0__cputime_")).toThrow();
expect(() => decodeFilter("0_cputime_1_2")).toThrow();
test("should throw errors if there are is only one '_' in the filter id", () => {
expect(() => decodeFilter("0cputime_")).toThrow();
expect(() => decodeFilter("0_cputime2")).toThrow();
});

test("should decode correctly with more than two '_' in the filter id", () => {
const filter = "0_cpu_time_1";
const expected = { tool: "0", name: "cpu_time", column: "1" };
expect(decodeFilter(filter)).toEqual(expected);
});
});

describe("tokenizePart", () => {
test("should tokenizePart to get Filter keys", () => {
const string = "id_any(value(%29)),0(1*cputime*(value(2)))";
const expected = { 0: "1*cputime*(value(2))", id_any: "value(%29)" };
expect(tokenizePart(string)).toEqual(expected);
});

test("should tokenizePart to get Filter values", () => {
const string = "value(%29)";
const expected = { value: ")" };
expect(tokenizePart(string, true)).toEqual(expected);
});
});

Expand Down Expand Up @@ -278,6 +319,34 @@ describe("serialization", () => {
expect(serializer(filter)).toBe(expected);
});

test("should serialize id filters with parentheses", () => {
const filter = [{ id: "id", values: ["(", ")"] }];
const expected = "id(values(%28,%29))";

expect(serializer(filter)).toBe(expected);
});

test("should serialize id filter to escape special characters", () => {
const filter = [{ id: "id", value: "?#&=(),*", isTableTabFilter: true }];
const expected = "id_any(value(%3F%23%26%3D%28%29%2C*))";

expect(serializer(filter)).toBe(expected);
});

test("should serialize id filter with one opening parentheses", () => {
const filter = [{ id: "id", value: "(", isTableTabFilter: true }];
const expected = "id_any(value(%28))";

expect(serializer(filter)).toBe(expected);
});

test("should serialize id filter with one closing parentheses", () => {
const filter = [{ id: "id", value: ")", isTableTabFilter: true }];
const expected = "id_any(value(%29))";

expect(serializer(filter)).toBe(expected);
});

test("should serialize normal value filters for one runset", () => {
const filter = [
{ id: "0_cputime_1", value: "1223:4567" },
Expand Down Expand Up @@ -598,6 +667,38 @@ describe("Filter deserialization", () => {
expect(deserializer(string)).toStrictEqual(expected);
});

test("should serialize id filters with parentheses", () => {
const string = "id(values(%28,%29))";

const expected = [{ id: "id", values: ["(", ")"] }];

expect(deserializer(string)).toStrictEqual(expected);
});

test("should deserialize id filter with one opening parentheses", () => {
const string = "id_any(value(%28))";

const expected = [{ id: "id", value: "(", isTableTabFilter: true }];

expect(deserializer(string)).toStrictEqual(expected);
});

test("should deserialize id filter with one closing parentheses", () => {
const string = "id_any(value(%29))";

const expected = [{ id: "id", value: ")", isTableTabFilter: true }];

expect(deserializer(string)).toStrictEqual(expected);
});

test("should deserialize Table Tab Id filter with special characters", () => {
const string = "id_any(value(%3F%23%26%3D()%2C*))*";

const expected = [{ id: "id", value: "?#&=(),*", isTableTabFilter: true }];

expect(deserializer(string)).toStrictEqual(expected);
});

test("should deserialize normal values for one runset", () => {
const string = "0(1*cputime*(value(%3A1234)))";

Expand Down
64 changes: 48 additions & 16 deletions benchexec/tablegenerator/react-table/src/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,14 @@ const EXTENDED_DISCRETE_COLOR_RANGE = [
];

/**
* Parses the search parameters from the URL hash or a provided string.
* Parses and decodes the search parameters except filter from the URL hash or a provided string.
*
* @param {string} - Optional string to parse. If not provided, parses the URL hash of the current document.
* @returns {Object} - An object containing the parsed search parameters.
*/
const getURLParameters = (str) => {
// Split the URL string into parts using "?" as a delimiter
const urlParts = (str || decodeURI(document.location.href)).split("?");
const urlParts = (str || document.location.href).split("?");
JawHawk marked this conversation as resolved.
Show resolved Hide resolved

// Extract the search part of the URL
const search = urlParts.length > 1 ? urlParts.slice(1).join("?") : undefined;
Expand All @@ -223,8 +223,11 @@ const getURLParameters = (str) => {
// Split the search string into key-value pairs and generate an object from them
const keyValuePairs = search.split("&").map((pair) => pair.split("="));
const out = {};

// All parameters in the search string are decoded except filter to allow filter handling later on its own
for (const [key, ...value] of keyValuePairs) {
out[key] = value.join("=");
out[decodeURI(key)] =
key === "filter" ? value.join("=") : decodeURI(value.join("="));
}

return out;
Expand Down Expand Up @@ -257,7 +260,10 @@ export const constructHashURL = (url, params = {}) => {
const queryString = constructQueryString(mergedParams);
const baseURL = url.split("?")[0];

return queryString.length > 0 ? `${baseURL}?${queryString}` : baseURL;
return {
newUrl: queryString.length > 0 ? `${baseURL}?${queryString}` : baseURL,
queryString: `?${queryString}`,
};
};

/**
Expand All @@ -269,10 +275,13 @@ export const constructHashURL = (url, params = {}) => {
* @returns {void}
*/
const setURLParameter = (params = {}, history = null) => {
const newUrl = constructHashURL(document.location.href, params);

const { newUrl, queryString } = constructHashURL(
document.location.href,
params,
);
if (history && history.push) {
history.push(newUrl);
history.push(queryString);
return;
}
document.location.href = newUrl;
};
Expand Down Expand Up @@ -363,6 +372,13 @@ function makeStatusColumnFilter(
return statusColumnFilter.join(",");
}

function escapeParentheses(value) {
if (typeof value !== "string") {
throw new Error("Invalid value type");
}
return value.replaceAll("(", "%28").replaceAll(")", "%29");
}

export const makeRegExp = (value) => {
if (typeof value !== "string") {
throw new Error("Invalid value type for converting to RegExp");
Expand All @@ -384,13 +400,18 @@ export const decodeFilter = (filterID) => {
}
const splitedArray = filterID.split("_");

if (splitedArray.length > 3) {
if (splitedArray.length === 2) {
throw new Error("Invalid filter ID");
}

// tool is always the first element value of the splitedArray
// column is always the last element value of the splitedArray
// name is the concatenation of remaining elements in between first and last element of splitedArray, separated by _
return {
tool: splitedArray[0],
name: splitedArray[1],
column: splitedArray[2],
name:
splitedArray.length > 2 ? splitedArray.slice(1, -1).join("_") : undefined,
column: splitedArray.length > 2 ? splitedArray.at(-1) : undefined,
PhilippWendler marked this conversation as resolved.
Show resolved Hide resolved
};
};

Expand Down Expand Up @@ -453,11 +474,19 @@ const makeFilterSerializer =
const { ids, ...rest } = groupedFilters;
const runsetFilters = [];
if (ids) {
runsetFilters.push(`id(values(${ids.values.map(escape).join(",")}))`);
runsetFilters.push(
`id(values(${ids.values
.map((val) => escapeParentheses(encodeURIComponent(val)))
.join(",")}))`,
);
}
if (tableTabIdFilters) {
tableTabIdFilters.forEach((filter) => {
runsetFilters.push(`id_any(value(${filter.value}))`);
runsetFilters.push(
`id_any(value(${escapeParentheses(
encodeURIComponent(filter.value),
)}))`,
);
});
}
for (const [tool, column] of Object.entries(rest)) {
Expand Down Expand Up @@ -490,7 +519,7 @@ const makeFilterSerializer =
return filterString;
};

const tokenizePart = (string) => {
export const tokenizePart = (string, decodeValue = false) => {
const out = {};
let openBrackets = 0;

Expand All @@ -513,7 +542,7 @@ const tokenizePart = (string) => {
firstBracket + 1,
buf.length - 1 - (firstBracket + 1),
);
out[key] = value;
out[key] = decodeValue ? decodeURIComponent(value) : value;
}
continue;
}
Expand Down Expand Up @@ -609,7 +638,8 @@ const makeFilterDeserializer =
} else if (token === "id_any") {
out.push({
id: "id",
...tokenizePart(filter),
...tokenizePart(filter, true),
isTableTabFilter: true,
PhilippWendler marked this conversation as resolved.
Show resolved Hide resolved
});
continue;
}
Expand All @@ -619,7 +649,9 @@ const makeFilterDeserializer =
const parsedColumnFilters = {};
for (const [key, columnFilter] of Object.entries(columnFilters)) {
const [columnId, columnTitle] = key.split("*");
const name = `${runsetId}_${unescape(columnTitle)}_${columnId}`;
const name = `${runsetId}_${decodeURIComponent(
columnTitle,
)}_${columnId}`;
const parsedFilters = parsedColumnFilters[name] || [];
const tokenizedFilter = tokenizePart(columnFilter);

Expand Down