@oino-ts/blob 1.0.6 → 1.0.7
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/dist/cjs/OINOBlob.js +15 -0
- package/dist/cjs/OINOBlobApi.js +3 -3
- package/dist/esm/OINOBlob.js +15 -0
- package/dist/esm/OINOBlobApi.js +3 -3
- package/dist/types/OINOBlob.d.ts +11 -0
- package/package.json +4 -4
- package/src/OINOBlobApi.test.ts +49 -2
package/dist/cjs/OINOBlob.js
CHANGED
|
@@ -10,6 +10,7 @@ const common_1 = require("@oino-ts/common");
|
|
|
10
10
|
const BLOB_LIKE_ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
|
|
11
11
|
const BLOB_LIKE_PERCENT_REGEX = /%/g;
|
|
12
12
|
const BLOB_LIKE_UNDERSCORE_REGEX = /_/g;
|
|
13
|
+
const BLOB_SANITIZE_DEFAULT_REGEX = /[\x00-\x1f\x7f]/g;
|
|
13
14
|
/**
|
|
14
15
|
* Abstract base class for blob storage backends. Subclasses implement
|
|
15
16
|
* the two core operations (`listEntries` and `fetchEntry`) for a specific
|
|
@@ -53,6 +54,20 @@ class OINOBlob extends common_1.OINODataSource {
|
|
|
53
54
|
}
|
|
54
55
|
return v;
|
|
55
56
|
}
|
|
57
|
+
// ── Blob name sanitization ──────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Sanitize a blob name by replacing characters that are illegal or unsafe
|
|
60
|
+
* on this storage backend with `_`.
|
|
61
|
+
*
|
|
62
|
+
* The base implementation strips ASCII control characters (U+0000–U+001F
|
|
63
|
+
* and U+007F). Subclasses should override to apply additional
|
|
64
|
+
* platform-specific rules.
|
|
65
|
+
*
|
|
66
|
+
* @param name raw blob name (path within the container)
|
|
67
|
+
*/
|
|
68
|
+
sanitizeName(name) {
|
|
69
|
+
return name.replace(BLOB_SANITIZE_DEFAULT_REGEX, "_");
|
|
70
|
+
}
|
|
56
71
|
// ── Blob-specific filter helper ───────────────────────────────────────
|
|
57
72
|
/**
|
|
58
73
|
* Test whether a blob entry matches an `OINOQueryFilter` predicate.
|
package/dist/cjs/OINOBlobApi.js
CHANGED
|
@@ -104,7 +104,7 @@ class OINOBlobApi extends common_1.OINOApi {
|
|
|
104
104
|
else {
|
|
105
105
|
// ── Download blob ────────────────────────────────────────────
|
|
106
106
|
try {
|
|
107
|
-
const name = decodeURIComponent(request.rowId);
|
|
107
|
+
const name = this.blob.sanitizeName(decodeURIComponent(request.rowId));
|
|
108
108
|
const fetch_result = await this.blob.fetchEntry(name);
|
|
109
109
|
result.blobData = fetch_result.content;
|
|
110
110
|
result.blobDataType = fetch_result.contentType;
|
|
@@ -121,7 +121,7 @@ class OINOBlobApi extends common_1.OINOApi {
|
|
|
121
121
|
}
|
|
122
122
|
else {
|
|
123
123
|
try {
|
|
124
|
-
const name = decodeURIComponent(request.rowId);
|
|
124
|
+
const name = this.blob.sanitizeName(decodeURIComponent(request.rowId));
|
|
125
125
|
const content_type = request.headers.get("content-type") ?? "application/octet-stream";
|
|
126
126
|
const data = request.rowData;
|
|
127
127
|
const content = data instanceof Uint8Array ? data : request.bodyAsBuffer();
|
|
@@ -139,7 +139,7 @@ class OINOBlobApi extends common_1.OINOApi {
|
|
|
139
139
|
}
|
|
140
140
|
else {
|
|
141
141
|
try {
|
|
142
|
-
const name = decodeURIComponent(request.rowId);
|
|
142
|
+
const name = this.blob.sanitizeName(decodeURIComponent(request.rowId));
|
|
143
143
|
await this.blob.deleteEntry(name);
|
|
144
144
|
}
|
|
145
145
|
catch (e) {
|
package/dist/esm/OINOBlob.js
CHANGED
|
@@ -7,6 +7,7 @@ import { OINODataSource, OINOQueryBooleanOperation, OINOQueryComparison, OINOQue
|
|
|
7
7
|
const BLOB_LIKE_ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
|
|
8
8
|
const BLOB_LIKE_PERCENT_REGEX = /%/g;
|
|
9
9
|
const BLOB_LIKE_UNDERSCORE_REGEX = /_/g;
|
|
10
|
+
const BLOB_SANITIZE_DEFAULT_REGEX = /[\x00-\x1f\x7f]/g;
|
|
10
11
|
/**
|
|
11
12
|
* Abstract base class for blob storage backends. Subclasses implement
|
|
12
13
|
* the two core operations (`listEntries` and `fetchEntry`) for a specific
|
|
@@ -50,6 +51,20 @@ export class OINOBlob extends OINODataSource {
|
|
|
50
51
|
}
|
|
51
52
|
return v;
|
|
52
53
|
}
|
|
54
|
+
// ── Blob name sanitization ──────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* Sanitize a blob name by replacing characters that are illegal or unsafe
|
|
57
|
+
* on this storage backend with `_`.
|
|
58
|
+
*
|
|
59
|
+
* The base implementation strips ASCII control characters (U+0000–U+001F
|
|
60
|
+
* and U+007F). Subclasses should override to apply additional
|
|
61
|
+
* platform-specific rules.
|
|
62
|
+
*
|
|
63
|
+
* @param name raw blob name (path within the container)
|
|
64
|
+
*/
|
|
65
|
+
sanitizeName(name) {
|
|
66
|
+
return name.replace(BLOB_SANITIZE_DEFAULT_REGEX, "_");
|
|
67
|
+
}
|
|
53
68
|
// ── Blob-specific filter helper ───────────────────────────────────────
|
|
54
69
|
/**
|
|
55
70
|
* Test whether a blob entry matches an `OINOQueryFilter` predicate.
|
package/dist/esm/OINOBlobApi.js
CHANGED
|
@@ -100,7 +100,7 @@ export class OINOBlobApi extends OINOApi {
|
|
|
100
100
|
else {
|
|
101
101
|
// ── Download blob ────────────────────────────────────────────
|
|
102
102
|
try {
|
|
103
|
-
const name = decodeURIComponent(request.rowId);
|
|
103
|
+
const name = this.blob.sanitizeName(decodeURIComponent(request.rowId));
|
|
104
104
|
const fetch_result = await this.blob.fetchEntry(name);
|
|
105
105
|
result.blobData = fetch_result.content;
|
|
106
106
|
result.blobDataType = fetch_result.contentType;
|
|
@@ -117,7 +117,7 @@ export class OINOBlobApi extends OINOApi {
|
|
|
117
117
|
}
|
|
118
118
|
else {
|
|
119
119
|
try {
|
|
120
|
-
const name = decodeURIComponent(request.rowId);
|
|
120
|
+
const name = this.blob.sanitizeName(decodeURIComponent(request.rowId));
|
|
121
121
|
const content_type = request.headers.get("content-type") ?? "application/octet-stream";
|
|
122
122
|
const data = request.rowData;
|
|
123
123
|
const content = data instanceof Uint8Array ? data : request.bodyAsBuffer();
|
|
@@ -135,7 +135,7 @@ export class OINOBlobApi extends OINOApi {
|
|
|
135
135
|
}
|
|
136
136
|
else {
|
|
137
137
|
try {
|
|
138
|
-
const name = decodeURIComponent(request.rowId);
|
|
138
|
+
const name = this.blob.sanitizeName(decodeURIComponent(request.rowId));
|
|
139
139
|
await this.blob.deleteEntry(name);
|
|
140
140
|
}
|
|
141
141
|
catch (e) {
|
package/dist/types/OINOBlob.d.ts
CHANGED
|
@@ -22,6 +22,17 @@ export declare abstract class OINOBlob extends OINODataSource {
|
|
|
22
22
|
printCellAsValue(cellValue: OINODataCell, _sqlType: string): string;
|
|
23
23
|
printStringValue(s: string): string;
|
|
24
24
|
parseValueAsCell(v: OINODataCell, nativeType: string): OINODataCell;
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize a blob name by replacing characters that are illegal or unsafe
|
|
27
|
+
* on this storage backend with `_`.
|
|
28
|
+
*
|
|
29
|
+
* The base implementation strips ASCII control characters (U+0000–U+001F
|
|
30
|
+
* and U+007F). Subclasses should override to apply additional
|
|
31
|
+
* platform-specific rules.
|
|
32
|
+
*
|
|
33
|
+
* @param name raw blob name (path within the container)
|
|
34
|
+
*/
|
|
35
|
+
sanitizeName(name: string): string;
|
|
25
36
|
/**
|
|
26
37
|
* Test whether a blob entry matches an `OINOQueryFilter` predicate.
|
|
27
38
|
* Used for in-memory (result) filtering when the storage backend cannot
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oino-ts/blob",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "OINO TS library package for publishing blob storage as a REST API.",
|
|
5
5
|
"author": "Matias Kiviniemi (pragmatta)",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"module": "./dist/esm/index.js",
|
|
20
20
|
"types": "./dist/types/index.d.ts",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@oino-ts/common": "1.0.
|
|
22
|
+
"@oino-ts/common": "1.0.7",
|
|
23
23
|
"oino-ts": "file:.."
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@oino-ts/types": "1.0.
|
|
26
|
+
"@oino-ts/types": "1.0.7",
|
|
27
27
|
"@types/bun": "^1.1.14",
|
|
28
|
-
"@types/node": "^21.0.
|
|
28
|
+
"@types/node": "^21.0.70",
|
|
29
29
|
"typescript": "~5.9.0"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
package/src/OINOBlobApi.test.ts
CHANGED
|
@@ -15,6 +15,15 @@ import { OINOBlob, OINOBlobApi, OINOBlobApiResult, OINOBlobFactory, type OINOBlo
|
|
|
15
15
|
const OINOCLOUD_TEST_BLOB_AZURE_CONSTR = process.env.OINOCLOUD_TEST_BLOB_AZURE_CONSTR || console.error("OINOCLOUD_TEST_BLOB_AZURE_CONSTR not set") || ""
|
|
16
16
|
const OINOCLOUD_TEST_BLOB_S3_CONSTR = process.env.OINOCLOUD_TEST_BLOB_S3_CONSTR || console.error("OINOCLOUD_TEST_BLOB_S3_CONSTR not set") || ""
|
|
17
17
|
|
|
18
|
+
type OINOBlobSanitizeTestCase = {
|
|
19
|
+
/** Human-readable description of what is being tested */
|
|
20
|
+
description: string
|
|
21
|
+
/** Input blob name possibly containing illegal or unsafe characters */
|
|
22
|
+
input: string
|
|
23
|
+
/** Expected output after sanitization */
|
|
24
|
+
expected: string
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
type OINOBlobStorageParams = {
|
|
19
28
|
/** Connection params passed to OINOBlobFactory.createBlob */
|
|
20
29
|
blobParams: OINOBlobParams
|
|
@@ -22,6 +31,8 @@ type OINOBlobStorageParams = {
|
|
|
22
31
|
apiName: string
|
|
23
32
|
/** Blob name prefix / folder used as tableName in the API */
|
|
24
33
|
prefix: string
|
|
34
|
+
/** Platform-specific sanitization test cases */
|
|
35
|
+
sanitizeTests: OINOBlobSanitizeTestCase[]
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
type OINOBlobTestParams = {
|
|
@@ -48,7 +59,16 @@ const BLOB_STORAGES: OINOBlobStorageParams[] = [
|
|
|
48
59
|
}
|
|
49
60
|
},
|
|
50
61
|
apiName: "azure-northwind",
|
|
51
|
-
prefix: "northwind-azure/"
|
|
62
|
+
prefix: "northwind-azure/",
|
|
63
|
+
sanitizeTests: [
|
|
64
|
+
{ description: "backslash replaced with underscore", input: "foo\\bar", expected: "foo_bar" },
|
|
65
|
+
{ description: "null byte replaced with underscore", input: "foo\x00bar", expected: "foo_bar" },
|
|
66
|
+
{ description: "unit-separator control char replaced with underscore", input: "foo\x1fbar", expected: "foo_bar" },
|
|
67
|
+
{ description: "DEL char replaced with underscore", input: "foo\x7fbar", expected: "foo_bar" },
|
|
68
|
+
{ description: "multiple illegal chars replaced", input: "foo\\bar\x00baz", expected: "foo_bar_baz" },
|
|
69
|
+
{ description: "forward slash preserved", input: "path/to/file.txt", expected: "path/to/file.txt" },
|
|
70
|
+
{ description: "valid safe chars unchanged", input: "file-name_1.2~3", expected: "file-name_1.2~3" }
|
|
71
|
+
]
|
|
52
72
|
},
|
|
53
73
|
{
|
|
54
74
|
blobParams: {
|
|
@@ -57,7 +77,22 @@ const BLOB_STORAGES: OINOBlobStorageParams[] = [
|
|
|
57
77
|
credentials: JSON.parse(OINOCLOUD_TEST_BLOB_S3_CONSTR)
|
|
58
78
|
},
|
|
59
79
|
apiName: "s3-northwind",
|
|
60
|
-
prefix: "northwind-s3/"
|
|
80
|
+
prefix: "northwind-s3/",
|
|
81
|
+
sanitizeTests: [
|
|
82
|
+
{ description: "backslash replaced with underscore", input: "foo\\bar", expected: "foo_bar" },
|
|
83
|
+
{ description: "null byte replaced with underscore", input: "foo\x00bar", expected: "foo_bar" },
|
|
84
|
+
{ description: "DEL char replaced with underscore", input: "foo\x7fbar", expected: "foo_bar" },
|
|
85
|
+
{ description: "curly braces replaced with underscores", input: "foo{bar}baz", expected: "foo_bar_baz" },
|
|
86
|
+
{ description: "square brackets replaced with underscores", input: "foo[bar]baz", expected: "foo_bar_baz" },
|
|
87
|
+
{ description: "caret replaced with underscore", input: "foo^bar", expected: "foo_bar" },
|
|
88
|
+
{ description: "backtick replaced with underscore", input: "foo`bar", expected: "foo_bar" },
|
|
89
|
+
{ description: "pipe replaced with underscore", input: "foo|bar", expected: "foo_bar" },
|
|
90
|
+
{ description: "angle brackets replaced with underscores", input: "foo<bar>baz", expected: "foo_bar_baz" },
|
|
91
|
+
{ description: "hash and percent replaced with underscores", input: "foo#bar%baz", expected: "foo_bar_baz" },
|
|
92
|
+
{ description: "forward slash preserved", input: "path/to/file.txt", expected: "path/to/file.txt" },
|
|
93
|
+
{ description: "valid safe chars unchanged", input: "file-name_1.2~3", expected: "file-name_1.2~3" },
|
|
94
|
+
{ description: "mixed illegal chars all replaced", input: "foo\\{bar}[baz]^`|<>#%qux", expected: "foo__bar__baz________qux" }
|
|
95
|
+
]
|
|
61
96
|
}
|
|
62
97
|
]
|
|
63
98
|
|
|
@@ -115,6 +150,18 @@ OINOBenchmark.reset()
|
|
|
115
150
|
OINOBlobFactory.registerBlob("OINOBlobAzure", OINOBlobAzure)
|
|
116
151
|
OINOBlobFactory.registerBlob("OINOBlobAwsS3", OINOBlobAwsS3)
|
|
117
152
|
|
|
153
|
+
// ── SANITIZE UNIT TESTS ───────────────────────────────────────────────────────
|
|
154
|
+
// These do not connect to any storage backend.
|
|
155
|
+
|
|
156
|
+
for (const storage of BLOB_STORAGES) {
|
|
157
|
+
const blob = await OINOBlobFactory.createBlob(storage.blobParams, false, false)
|
|
158
|
+
for (const tc of storage.sanitizeTests) {
|
|
159
|
+
test("[SANITIZE][" + storage.blobParams.type + "] " + tc.description, () => {
|
|
160
|
+
expect(blob.sanitizeName(tc.input)).toBe(tc.expected)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
118
165
|
function encodeResult(o: unknown): string {
|
|
119
166
|
return JSON.stringify(o ?? {}, null, 3)
|
|
120
167
|
.replaceAll(/`/g, "'")
|