Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion handwritten/storage/src/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import * as path from 'path';
import pLimit from 'p-limit';
import {promisify} from 'util';
import AsyncRetry from 'async-retry';
import {convertObjKeysToSnakeCase} from './util.js';
import {convertObjKeysToSnakeCase, validateContexts} from './util.js';

import {Acl, AclMetadata} from './acl.js';
import {Channel} from './channel.js';
Expand All @@ -44,6 +44,7 @@ import {
CreateResumableUploadOptions,
CreateWriteStreamOptions,
FileMetadata,
ContextValue,
} from './file.js';
import {Iam} from './iam.js';
import {Notification, NotificationMetadata} from './notification.js';
Expand Down Expand Up @@ -178,11 +179,17 @@ export interface GetFilesOptions {
userProject?: string;
versions?: boolean;
fields?: string;
filter?: string;
}

export interface CombineOptions extends PreconditionOptions {
kmsKeyName?: string;
userProject?: string;
contexts?: {
custom: {
[key: string]: ContextValue;
} | null;
};
}

export interface CombineCallback {
Expand Down Expand Up @@ -1628,6 +1635,17 @@ class Bucket extends ServiceObject<Bucket, BucketMetadata> {
options = optionsOrCallback;
}

if (options.contexts) {
try {
validateContexts(options.contexts);
} catch (err) {
if (callback) {
return (callback as CombineCallback)(err as Error, null, null);
}
return Promise.reject(err);
}
}

this.disableAutoRetryConditionallyIdempotent_(
this.methods.setMetadata, // Not relevant but param is required
AvailableServiceObjectMethods.setMetadata, // Same as above
Expand Down Expand Up @@ -1682,6 +1700,7 @@ class Bucket extends ServiceObject<Bucket, BucketMetadata> {
destination: {
contentType: destinationFile.metadata.contentType,
contentEncoding: destinationFile.metadata.contentEncoding,
contexts: options.contexts || destinationFile.metadata.contexts,
},
sourceObjects: (sources as File[]).map(source => {
const sourceObject = {
Expand Down
42 changes: 42 additions & 0 deletions handwritten/storage/src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
unicodeJSONStringify,
formatAsUTCISO,
PassThroughShim,
validateContexts,
} from './util.js';
import {CRC32C, CRC32CValidatorGenerator} from './crc32c.js';
import {HashStreamValidator} from './hash-stream-validator.js';
Expand Down Expand Up @@ -382,6 +383,11 @@ export interface CopyOptions {
metadata?: {
[key: string]: string | boolean | number | null;
};
contexts?: {
custom: {
[key: string]: ContextValue;
} | null;
};
predefinedAcl?: string;
token?: string;
userProject?: string;
Expand Down Expand Up @@ -469,6 +475,12 @@ export interface RestoreOptions extends PreconditionOptions {
projection?: 'full' | 'noAcl';
}

export interface ContextValue {
value: string | null;
readonly createTime?: string;
readonly updateTime?: string;
}

export interface FileMetadata extends BaseMetadata {
acl?: AclMetadata[] | null;
bucket?: string;
Expand All @@ -483,6 +495,11 @@ export interface FileMetadata extends BaseMetadata {
encryptionAlgorithm?: string;
keySha256?: string;
};
contexts?: {
custom: {
[key: string]: ContextValue | null;
} | null;
};
customTime?: string;
eventBasedHold?: boolean | null;
readonly eventBasedHoldReleaseTime?: string;
Expand Down Expand Up @@ -1292,6 +1309,17 @@ class File extends ServiceObject<File, FileMetadata> {
options = {...optionsOrCallback};
}

if (options.contexts) {
try {
validateContexts(options.contexts);
} catch (err) {
if (callback) {
return (callback as CopyCallback)(err as Error, null, null);
}
return Promise.reject(err);
}
}

callback = callback || util.noop;

let destBucket: Bucket;
Expand Down Expand Up @@ -4127,6 +4155,13 @@ class File extends ServiceObject<File, FileMetadata> {
const options =
typeof optionsOrCallback === 'object' ? optionsOrCallback : {};

try {
validateContexts(options.metadata?.contexts);
} catch (err) {
if (callback) return callback(err as Error);
return Promise.reject(err);
}

let maxRetries = this.storage.retryOptions.maxRetries;
if (
!this.shouldRetryBasedOnPreconditionAndIdempotencyStrat(
Expand Down Expand Up @@ -4230,6 +4265,13 @@ class File extends ServiceObject<File, FileMetadata> {
? (optionsOrCallback as MetadataCallback<FileMetadata>)
: cb;

try {
validateContexts(metadata.contexts);
} catch (err) {
if (cb) return cb(err as Error);
return Promise.reject(err);
}

this.disableAutoRetryConditionallyIdempotent_(
this.methods.setMetadata,
AvailableServiceObjectMethods.setMetadata,
Expand Down
27 changes: 27 additions & 0 deletions handwritten/storage/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as url from 'url';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import {getPackageJSON} from './package-json-helper.cjs';
import {FileMetadata} from './file';

// Done to avoid a problem with mangling of identifiers when using esModuleInterop
const fileURLToPath = url.fileURLToPath;
Expand Down Expand Up @@ -272,3 +273,29 @@ export class PassThroughShim extends PassThrough {
callback(null);
}
}


/**
* Validates Object Contexts for forbidden characters.
* Double quotes (") are forbidden in context keys and values as they
* interfere with GCS filter string syntax.
*
* @param {FileMetadata['contexts']} contexts The contexts object to validate.
* @returns {void} Throws an error if validation fails.
*/
export function validateContexts(contexts?: FileMetadata['contexts']): void {
const custom = contexts?.custom;
if (!custom) return;
for (const [key, context] of Object.entries(custom)) {
if (key.includes('"')) {
throw new Error(
`Invalid context key "${key}": Forbidden character (") detected.`
);
}
if (context?.value && context.value.includes('"')) {
throw new Error(
`Invalid context value for key "${key}": Forbidden character (") detected.`
);
}
}
}
Loading