Skip to content

Commit 5d55028

Browse files
authored
fix: truncate path parts (#66)
1 parent 615d792 commit 5d55028

File tree

3 files changed

+166
-10
lines changed

3 files changed

+166
-10
lines changed
Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,121 @@
11
import { describe, expect, it } from '@jest/globals';
2-
import { sanitizeFilename } from '../lib';
2+
import {
3+
MAX_FILE_PATH_LENGTH,
4+
isValidLength,
5+
sanitizeFilename,
6+
truncateMiddle,
7+
truncatePathParts,
8+
} from '../lib';
9+
10+
describe('isValidLength', () => {
11+
const testCases: [string, number | undefined, boolean][] = [
12+
['short.txt', undefined, true], // Valid filename with default maxLength
13+
['a'.repeat(MAX_FILE_PATH_LENGTH), undefined, true], // Valid filename with maximum maxLength
14+
['a'.repeat(MAX_FILE_PATH_LENGTH + 1), undefined, false], // Invalid filename exceeding maxLength
15+
['abcdefghij', 10, true], // Valid filename with specified maxLength
16+
['abcdefghijk', 10, false], // Invalid filename exceeding specified maxLength
17+
['anystring.txt', 0, false], // Invalid filename with maxLength of 0
18+
];
19+
20+
it.each(testCases)(
21+
'should return the exepcted result for the provided string and max length',
22+
(str: string, maxLength: number | undefined, expectedResult: boolean) => {
23+
expect(isValidLength(str, maxLength)).toBe(expectedResult);
24+
}
25+
);
26+
});
27+
28+
describe('truncateMiddle', () => {
29+
const longString =
30+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ac odio ac quam auctor faucibus ut id dolor. Vivamus vel odio eu ligula tempus viverra. Aenean vehicula, ex non varius euismod, elit ex cursus ex, in bibendum quam elit quis tortor. Sed volutpat scelerisque tortor quis blandit. A';
31+
const truncatedString =
32+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ac odio ac quam auctor faucibus ut id dolor. Vivamus vel o..., ex non varius euismod, elit ex cursus ex, in bibendum quam elit quis tortor. Sed volutpat scelerisque tortor quis blandit. A';
33+
const testCases = [
34+
['short.txt', MAX_FILE_PATH_LENGTH, 'short.txt'], // Short filename within maximum length
35+
[longString, MAX_FILE_PATH_LENGTH, truncatedString], // Long filename, truncated with default separator
36+
['short.txt', 5, 's...t'], // Short filename within a custom maximum length
37+
['longname.txt', 8, 'lon...xt'], // Long filename truncated with custom maximum length and default separator
38+
['filename.txt', 10, 'file...txt'], // Medium-length filename truncated with default separator
39+
['middleseparator.txt', 15, 'middle...or.txt'], // Medium-length filename truncated with default separator
40+
['separatoratstart.txt', 15, 'separa...rt.txt'], // Medium-length filename truncated with default separator
41+
] as Array<[string, number, string]>;
42+
43+
it.each(testCases)(
44+
'should truncate the string correctly for the provided filename and max length',
45+
(filename, maxLength, expected) => {
46+
const result = truncateMiddle(filename, maxLength);
47+
expect(result).toBe(expected);
48+
}
49+
);
50+
51+
const truncatedStringWithDashes =
52+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ac odio ac quam auctor faucibus ut id dolor. Vivamus vel od--, ex non varius euismod, elit ex cursus ex, in bibendum quam elit quis tortor. Sed volutpat scelerisque tortor quis blandit. A';
53+
54+
const customSeparatorTestCases = [
55+
['short.txt', MAX_FILE_PATH_LENGTH, 'short.txt'], // Short filename within maximum length
56+
[longString, MAX_FILE_PATH_LENGTH, truncatedStringWithDashes], // Long filename, truncated with default separator
57+
['short.txt', 5, 'sh--t'], // Short filename within a custom maximum length
58+
['longname.txt', 8, 'lon--txt'], // Long filename truncated with custom maximum length and default separator
59+
['filename.txt', 10, 'file--.txt'], // Medium-length filename truncated with default separator
60+
['middleseparator.txt', 15, 'middles--or.txt'], // Medium-length filename truncated with default separator
61+
['separatoratstart.txt', 15, 'separat--rt.txt'], // Medium-length filename truncated with default separator
62+
] as Array<[string, number, string]>;
63+
64+
it.each(customSeparatorTestCases)(
65+
'truncates "%s" to "%s" with max length %d and custom separator',
66+
(filename, maxLength, expected) => {
67+
const result = truncateMiddle(filename, maxLength, '--');
68+
expect(result).toBe(expected);
69+
}
70+
);
71+
});
372

473
describe('sanitizeFilename', () => {
5-
it('should sanitize the string', () => {
6-
expect(sanitizeFilename('a/b\\c?d*e:f|g<h>i%jk', ' ')).toEqual(
7-
'a b c d e f g h i jk'
8-
);
74+
const testCases = [
75+
['validFilename.txt', 'validFilename.txt'], // Valid filename with no invalid characters
76+
['file?with?question?mark.txt', 'file-with-question-mark.txt'], // Replace '?' with '-'
77+
['file*with*asterisk.txt', 'file-with-asterisk.txt'], // Replace '*' with '-'
78+
['file<with<less.txt', 'file-with-less.txt'], // Replace '<' with '-'
79+
['file>with>greater.txt', 'file-with-greater.txt'], // Replace '>' with '-'
80+
['file:with:colon.txt', 'file-with-colon.txt'], // Replace ':' with '-'
81+
['file|with|pipe.txt', 'file-with-pipe.txt'], // Replace '|' with '-'
82+
['file%with%percent.txt', 'file-with-percent.txt'], // Replace '%' with '-'
83+
['file"with"doublequote.txt', 'file-with-doublequote.txt'], // Replace '"' with '-'
84+
['file/with/slash.txt', 'file-with-slash.txt'], // Replace '/' with '-'
85+
['file\\with\\backslash.txt', 'file-with-backslash.txt'], // Replace '\\' with '-'
86+
[
87+
'mixed?chars*and<slashes>and:stuff|"here%.txt',
88+
'mixed-chars-and-slashes-and-stuff--here-.txt',
89+
], // Replace multiple invalid characters with '-'
90+
];
91+
92+
it.each(testCases)(
93+
'should sanitize the string as expected',
94+
(input, expected) => {
95+
const result = sanitizeFilename(input);
96+
expect(result).toBe(expected);
97+
}
98+
);
99+
});
100+
101+
describe('truncatePathParts function', () => {
102+
const longFolderName =
103+
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu';
104+
const truncatedLongFolderName =
105+
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu';
106+
const longFilename =
107+
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopq.json';
108+
const truncatedLongFilename =
109+
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst...yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopq.json';
110+
it.each([
111+
['folder1/file.txt', 'folder1/file.txt'],
112+
[
113+
`${longFolderName}/${longFolderName}/folder3/file.txt`,
114+
`${truncatedLongFolderName}/${truncatedLongFolderName}/folder3/file.txt`,
115+
],
116+
[`folder1/${longFilename}`, `folder1/${truncatedLongFilename}`],
117+
])('should truncate path parts', (input, expected) => {
118+
const result = truncatePathParts(input);
119+
expect(result).toEqual(expected);
9120
});
10121
});

packages/plugin/src/install.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
recordLogs,
1212
} from './browserLogs';
1313
import { createDir, readFile, removeDir, removeFile, writeFile } from './fs';
14-
import { sanitizeFilename } from './lib';
14+
import { sanitizeFilename, truncatePathParts } from './lib';
1515
import { PluginOptions, TestExecutionResult } from './types';
1616

1717
const harDir = 'dump_har';
@@ -30,7 +30,7 @@ const createDumpFile = (data: TestExecutionResult, dumpDir: string): string => {
3030
specDirPath,
3131
`${sanitizeFilename(filename)}.json`
3232
);
33-
writeFile(resultsPath, JSON.stringify(data, null, 2));
33+
writeFile(truncatePathParts(resultsPath), JSON.stringify(data, null, 2));
3434
return resultsPath;
3535
};
3636

@@ -83,7 +83,7 @@ function install(
8383
const dumpDir =
8484
options?.targetDirectory &&
8585
path.resolve(options.targetDirectory) !== path.resolve(harDir)
86-
? options.targetDirectory
86+
? truncatePathParts(options.targetDirectory)
8787
: 'dump';
8888

8989
const resultsFilePath = createDumpFile(dumpData, dumpDir);

packages/plugin/src/lib.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,48 @@
1+
import path from 'path';
2+
3+
// https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
4+
const invalidChars = /[/?<>\\:*|%"]/g;
5+
6+
export const MAX_FILE_PATH_LENGTH = 255;
7+
8+
export const isValidLength = (str: string, maxLength = MAX_FILE_PATH_LENGTH) =>
9+
Buffer.from(str, 'utf-8').length <= maxLength;
10+
11+
export const truncateMiddle = (
12+
filename: string,
13+
maxLength: number = MAX_FILE_PATH_LENGTH,
14+
separator = '...'
15+
) => {
16+
if (isValidLength(filename, maxLength)) return filename.trim();
17+
18+
const sepLen = separator.length;
19+
const charsToShow = maxLength - sepLen;
20+
const frontChars = Math.ceil(charsToShow / 2);
21+
const backChars = Math.floor(charsToShow / 2);
22+
23+
return (
24+
filename.substring(0, frontChars) +
25+
separator +
26+
filename.substring(filename.length - backChars)
27+
);
28+
};
29+
130
export const sanitizeFilename = (filename: string, replacement = '-') =>
2-
// https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
3-
filename.replace(/[/?<>\\:*|%"]/g, replacement);
31+
filename.replace(invalidChars, replacement);
32+
33+
export const truncatePathParts = (filename: string): string =>
34+
filename
35+
.split(path.sep)
36+
.map((s, i, arr) => {
37+
if (i === arr.length - 1) {
38+
const ext = path.extname(s);
39+
const filenameWithoutExt = path.parse(path.basename(s)).name;
40+
return truncateMiddle(
41+
filenameWithoutExt,
42+
MAX_FILE_PATH_LENGTH - ext.length
43+
).concat(ext);
44+
}
45+
46+
return truncateMiddle(s);
47+
})
48+
.join(path.sep);

0 commit comments

Comments
 (0)