Skip to content

Commit 9f43a66

Browse files
committed
fix: support encoding of special characters in path params for gen2 funcs
1 parent 85344d0 commit 9f43a66

2 files changed

Lines changed: 117 additions & 1 deletion

File tree

src/cloud_events.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ export function getBinaryCloudEventContext(
6565
const attributeName = name.substring(
6666
'ce-'.length,
6767
) as keyof CloudEvent<unknown>;
68-
context[attributeName] = req.header(name);
68+
const val = req.header(name);
69+
context[attributeName] = val
70+
? Buffer.from(val, 'binary').toString('utf-8')
71+
: val;
6972
}
7073
}
7174
return context;

test/cloud_events.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import * as assert from 'assert';
16+
import {Request} from 'express';
17+
import {getBinaryCloudEventContext} from '../src/cloud_events';
18+
19+
// Simulates what Node.js HTTP parsing produces when UTF-8-encoded bytes arrive
20+
// in a header and are decoded as Latin-1 (binary). This is what req.header()
21+
// returns in practice when non-ASCII Unicode is sent in CloudEvent extension
22+
// attributes by services like Firestore.
23+
function asNodeHttpHeader(s: string): string {
24+
return Buffer.from(s, 'utf-8').toString('binary');
25+
}
26+
27+
function makeMockRequest(headers: Record<string, string>): Request {
28+
const lowercased = Object.fromEntries(
29+
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]),
30+
);
31+
return {
32+
headers: lowercased,
33+
header: (name: string) => lowercased[name.toLowerCase()],
34+
} as unknown as Request;
35+
}
36+
37+
describe('getBinaryCloudEventContext', () => {
38+
describe('binary-encoded UTF-8 extension attributes (Firestore path params)', () => {
39+
const cases = [
40+
{
41+
name: 'latin script with diacritics',
42+
input: 'Helvétios',
43+
expected: 'Helvétios',
44+
},
45+
{
46+
name: 'CJK text',
47+
input: asNodeHttpHeader('中文'),
48+
expected: '中文',
49+
},
50+
{
51+
name: 'emoji',
52+
input: asNodeHttpHeader('😀'),
53+
expected: '😀',
54+
},
55+
{
56+
name: 'Hindi text',
57+
input: asNodeHttpHeader('हिन्दी'),
58+
expected: 'हिन्दी',
59+
},
60+
{
61+
name: 'Arabic text',
62+
input: asNodeHttpHeader('اَلْعَرَبِيَّةُ'),
63+
expected: 'اَلْعَرَبِيَّةُ',
64+
},
65+
];
66+
67+
for (const {name, input, expected} of cases) {
68+
it(`decodes ${name}`, () => {
69+
const req = makeMockRequest({
70+
'ce-specversion': '1.0',
71+
'ce-type': 'google.cloud.firestore.document.v1.created',
72+
'ce-source': '//firestore.googleapis.com/projects/my-project',
73+
'ce-id': 'test-id',
74+
'ce-document': input,
75+
});
76+
const context = getBinaryCloudEventContext(req);
77+
assert.strictEqual(context['document' as keyof typeof context], expected);
78+
});
79+
}
80+
81+
it('decodes mojibake embedded in a longer path', () => {
82+
const req = makeMockRequest({
83+
'ce-specversion': '1.0',
84+
'ce-type': 'google.cloud.firestore.document.v1.created',
85+
'ce-source': '//firestore.googleapis.com/projects/my-project',
86+
'ce-id': 'test-id',
87+
'ce-document': asNodeHttpHeader('users/Helvétios/posts/café'),
88+
});
89+
const context = getBinaryCloudEventContext(req);
90+
assert.strictEqual(
91+
context['document' as keyof typeof context],
92+
'users/Helvétios/posts/café',
93+
);
94+
});
95+
});
96+
97+
describe('ASCII extension attributes', () => {
98+
it('passes through ASCII values without modification', () => {
99+
const req = makeMockRequest({
100+
'ce-specversion': '1.0',
101+
'ce-type': 'google.cloud.firestore.document.v1.created',
102+
'ce-source': '//firestore.googleapis.com/projects/my-project',
103+
'ce-id': 'test-id',
104+
'ce-document': 'users/hello-world',
105+
});
106+
const context = getBinaryCloudEventContext(req);
107+
assert.strictEqual(
108+
context['document' as keyof typeof context],
109+
'users/hello-world',
110+
);
111+
});
112+
});
113+
});

0 commit comments

Comments
 (0)