@oino-ts/blob 1.0.5 → 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.
@@ -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.
@@ -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) {
@@ -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.
@@ -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) {
@@ -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.5",
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.5",
22
+ "@oino-ts/common": "1.0.7",
23
23
  "oino-ts": "file:.."
24
24
  },
25
25
  "devDependencies": {
26
- "@oino-ts/types": "1.0.5",
26
+ "@oino-ts/types": "1.0.7",
27
27
  "@types/bun": "^1.1.14",
28
- "@types/node": "^21.0.50",
28
+ "@types/node": "^21.0.70",
29
29
  "typescript": "~5.9.0"
30
30
  },
31
31
  "files": [
package/src/OINOBlob.ts CHANGED
@@ -11,6 +11,8 @@ const BLOB_LIKE_ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g
11
11
  const BLOB_LIKE_PERCENT_REGEX = /%/g
12
12
  const BLOB_LIKE_UNDERSCORE_REGEX = /_/g
13
13
 
14
+ const BLOB_SANITIZE_DEFAULT_REGEX = /[\x00-\x1f\x7f]/g
15
+
14
16
  /**
15
17
  * Abstract base class for blob storage backends. Subclasses implement
16
18
  * the two core operations (`listEntries` and `fetchEntry`) for a specific
@@ -62,6 +64,22 @@ export abstract class OINOBlob extends OINODataSource {
62
64
  return v
63
65
  }
64
66
 
67
+ // ── Blob name sanitization ──────────────────────────────────────────────
68
+
69
+ /**
70
+ * Sanitize a blob name by replacing characters that are illegal or unsafe
71
+ * on this storage backend with `_`.
72
+ *
73
+ * The base implementation strips ASCII control characters (U+0000–U+001F
74
+ * and U+007F). Subclasses should override to apply additional
75
+ * platform-specific rules.
76
+ *
77
+ * @param name raw blob name (path within the container)
78
+ */
79
+ sanitizeName(name: string): string {
80
+ return name.replace(BLOB_SANITIZE_DEFAULT_REGEX, "_")
81
+ }
82
+
65
83
  // ── Blob-specific filter helper ───────────────────────────────────────
66
84
 
67
85
  /**
@@ -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, "'")
@@ -286,6 +333,7 @@ export async function OINOTestBlob(storageParams: OINOBlobStorageParams, testPar
286
333
  expect(verify_result.success).toBe(true)
287
334
  expect(verify_result.blobData).toBeDefined()
288
335
  expect(new TextDecoder().decode(verify_result.blobData)).toBe(new TextDecoder().decode(testParams.uploadContent))
336
+ expect(verify_result.blobDataType).toBe(testParams.uploadContentType)
289
337
  })
290
338
 
291
339
  // ── UPDATE (PUT) ──────────────────────────────────────────────────────
@@ -326,6 +374,7 @@ export async function OINOTestBlob(storageParams: OINOBlobStorageParams, testPar
326
374
  const verify_result: OINOBlobApiResult = await api.doApiRequest(verify_request)
327
375
  expect(verify_result.success).toBe(true)
328
376
  expect(new TextDecoder().decode(verify_result.blobData)).toBe(new TextDecoder().decode(testParams.updateContent))
377
+ expect(verify_result.blobDataType).toBe(testParams.uploadContentType)
329
378
  })
330
379
 
331
380
  // ── DELETE ────────────────────────────────────────────────────────────
@@ -126,7 +126,7 @@ export class OINOBlobApi extends OINOApi {
126
126
  } else {
127
127
  // ── Download blob ────────────────────────────────────────────
128
128
  try {
129
- const name = decodeURIComponent(request.rowId)
129
+ const name = this.blob.sanitizeName(decodeURIComponent(request.rowId))
130
130
  const fetch_result = await this.blob.fetchEntry(name)
131
131
  result.blobData = fetch_result.content
132
132
  result.blobDataType = fetch_result.contentType
@@ -142,7 +142,7 @@ export class OINOBlobApi extends OINOApi {
142
142
  result.setError(400, "HTTP " + request.method + " method requires an URL ID (blob name)!", "DoRequest")
143
143
  } else {
144
144
  try {
145
- const name = decodeURIComponent(request.rowId)
145
+ const name = this.blob.sanitizeName(decodeURIComponent(request.rowId))
146
146
  const content_type = request.headers.get("content-type") ?? "application/octet-stream"
147
147
  const data = request.rowData
148
148
  const content: Uint8Array = data instanceof Uint8Array ? data : request.bodyAsBuffer()
@@ -159,7 +159,7 @@ export class OINOBlobApi extends OINOApi {
159
159
  result.setError(400, "HTTP DELETE method requires an URL ID (blob name)!", "DoRequest")
160
160
  } else {
161
161
  try {
162
- const name = decodeURIComponent(request.rowId)
162
+ const name = this.blob.sanitizeName(decodeURIComponent(request.rowId))
163
163
  await this.blob.deleteEntry(name)
164
164
  } catch (e: any) {
165
165
  result.setError(500, "Error deleting blob: " + e.message, "DoDelete")