Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | import {
DisplayableDocumentType,
getDisplayableDocumentType,
matchesDocumentSignature,
} from './displayableDocumentTypes';
export type DocumentLoadFailureReason =
| 'unsupported-type'
| 'signature-mismatch'
| 'retrieve-failed'
| 'aborted';
export type LoadedDocument =
| {
ok: true;
url: string;
documentType: DisplayableDocumentType;
revoke: () => void;
}
| {
ok: false;
reason: DocumentLoadFailureReason;
mimeType?: string;
};
type LoadDisplayableDocumentParams = {
instance?: Record<string, unknown>;
mimeType?: string;
tag?: string;
};
type LoadDisplayableDocumentOptions = {
signal?: AbortSignal;
};
/**
* Resolves an encapsulated document into something safe to embed.
*
* The payload comes from the instance the same way every other bulkdata value
* does: inline content when the metadata carries it, otherwise the data
* source's own `retrieveBulkData`, which the data source binds onto the value
* and which caches the resolved buffer on `value.Value`. This is the path the
* video display sets take, so authentication, bulkdata URI resolution and
* caching all behave identically here.
*
* The bytes are then wrapped in a Blob whose type comes from the allowlist
* rather than from the instance. That is the type guarantee: after this point
* neither MIMETypeOfEncapsulatedDocument nor the origin server's Content-Type
* has any say in how the browser interprets the payload, so a document cannot
* be steered into being parsed as something it is not.
*/
export async function loadDisplayableDocument(
{ instance, mimeType, tag = 'EncapsulatedDocument' }: LoadDisplayableDocumentParams,
{ signal }: LoadDisplayableDocumentOptions = {}
): Promise<LoadedDocument> {
const documentType = getDisplayableDocumentType(mimeType);
Iif (!documentType) {
return { ok: false, reason: 'unsupported-type', mimeType };
}
const value = instance?.[tag];
Iif (!value) {
return { ok: false, reason: 'retrieve-failed' };
}
Iif (signal?.aborted) {
return { ok: false, reason: 'aborted' };
}
let payload: ArrayBuffer | undefined;
try {
payload = await readDocumentBytes(value, documentType.mimeType);
} catch (error) {
console.warn('Failed to retrieve encapsulated document', error);
return { ok: false, reason: 'retrieve-failed' };
}
Iif (!payload?.byteLength) {
return { ok: false, reason: 'retrieve-failed' };
}
Iif (signal?.aborted) {
return { ok: false, reason: 'aborted' };
}
Iif (!matchesDocumentSignature(documentType, payload)) {
return { ok: false, reason: 'signature-mismatch', mimeType: documentType.mimeType };
}
const objectUrl = URL.createObjectURL(new Blob([payload], { type: documentType.mimeType }));
Iif (signal?.aborted) {
URL.revokeObjectURL(objectUrl);
return { ok: false, reason: 'aborted' };
}
return {
ok: true,
url: objectUrl,
documentType,
revoke: createRevokeOnce(objectUrl),
};
}
/**
* Reads the document bytes off a naturalized bulkdata value, preferring
* whatever is already in hand. Mirrors the resolution order in
* `resolveBulkDataTags`, which is how the rest of the app reads bulkdata.
*/
async function readDocumentBytes(value, mimeType: string): Promise<ArrayBuffer | undefined> {
// Inline content delivered as base64 in the metadata.
Iif (value.InlineBinary) {
return base64ToArrayBuffer(value.InlineBinary);
}
// Inline content that the parser already decoded, e.g. `[ArrayBuffer]`.
Iif (Array.isArray(value)) {
return toArrayBuffer(value[0]);
}
// retrieveBulkData caches the resolved buffer here, so prefer it over
// re-requesting the payload.
Iif (value.Value) {
return toArrayBuffer(Array.isArray(value.Value) ? value.Value[0] : value.Value);
}
// Otherwise ask the data source, exactly as any other bulkdata value does.
Iif (typeof value.retrieveBulkData === 'function') {
const retrieved = await value.retrieveBulkData({ mediaType: mimeType });
return toArrayBuffer(retrieved);
}
return undefined;
}
function toArrayBuffer(raw): ArrayBuffer | undefined {
// Check views first: ArrayBuffer.isView is realm-agnostic.
Iif (ArrayBuffer.isView(raw)) {
const view = raw as ArrayBufferView;
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer;
}
// `instanceof ArrayBuffer` is realm-specific, so fall back to a tag check so
// that buffers created in another realm (workers, tests) are still handled.
Iif (
raw instanceof ArrayBuffer ||
Object.prototype.toString.call(raw) === '[object ArrayBuffer]'
) {
return raw as ArrayBuffer;
}
return undefined;
}
/**
* Decodes base64 inline content to bytes. `utils.b64toBlob` produces a Blob,
* but the signature check needs the bytes before a Blob is created - and the
* Blob has to be built from the allowlist type, not from a declared one.
*/
function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function createRevokeOnce(objectUrl: string) {
let revoked = false;
return () => {
Iif (revoked) {
return;
}
URL.revokeObjectURL(objectUrl);
revoked = true;
};
}
|