All files / extensions/dicom-pdf/src/utils displayableDocumentTypes.ts

12% Statements 3/25
0% Branches 0/10
0% Functions 0/4
13.04% Lines 3/23

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                                                                                                        199x             199x                                                                             199x                                                                                                                                                
/**
 * The set of encapsulated-document MIME types this extension is willing to put
 * in front of a user, and how each one is embedded.
 *
 * MIMETypeOfEncapsulatedDocument is supplied by whoever produced the instance,
 * so it is treated here as a claim to be checked rather than an instruction to
 * be followed. A type that is not on this list is not rendered at all.
 */
 
/**
 * How a document is embedded in the viewport.
 *
 * 'object' - <object>. The browsers' built-in PDF viewers refuse to run inside
 *   a sandboxed browsing context: in Chrome a PDF renders under no sandbox at
 *   all and under none of the token combinations, including the fully
 *   permissive `allow-scripts allow-same-origin`. PDFs therefore cannot be
 *   sandboxed, and their safety rests entirely on the type guarantee that
 *   loadDisplayableDocument applies (canonical Blob type + signature check).
 *
 * 'iframe' - <iframe sandbox>. Markup types render correctly inside a sandbox,
 *   so they get one. The default is the empty sandbox: no tokens, scripts
 *   inert, opaque origin, no access to the viewer's storage or DOM.
 */
export type DocumentEmbedStrategy = 'object' | 'iframe';
 
export type DisplayableDocumentType = {
  /** Canonical type forced onto the Blob handed to the browser. */
  mimeType: string;
  strategy: DocumentEmbedStrategy;
  /** sandbox attribute value; only meaningful for the 'iframe' strategy. */
  sandbox?: string;
  /** Magic number every payload of this type has to carry, when the type has a
   *  reliable one. Types without one rely on the sandbox instead. */
  signature?: number[];
  /** How far into the payload the signature is allowed to start. Absent means
   *  offset 0 - see PDF_SIGNATURE_SEARCH_LIMIT for why a type wants slack. */
  signatureSearchLimit?: number;
};
 
/**
 * Real files do not always put their magic number at byte 0, and readers that
 * accept them anyway are the reason such files exist in the wild.
 *
 * PDF: ISO 32000-1 requires "%PDF-" at the start of the file, but Adobe's own
 * implementation note relaxes this to anywhere within the first 1024 bytes, and
 * every mainstream reader follows suit. Producers that write a UTF-8 BOM, or
 * that prepend junk before the header, therefore produce files that open
 * everywhere except here. Matching the 1024-byte allowance keeps the check
 * doing its actual job - catching a payload that is not a PDF at all, which is
 * how a document declared as PDF but containing markup gets rejected - without
 * failing valid documents over leading bytes the renderer will skip anyway.
 */
const PDF_SIGNATURE_SEARCH_LIMIT = 1024;
 
/**
 * Canonical entries, keyed by canonical MIME type. Exported so downstream
 * deployments can extend the list; adding an entry means asserting both that
 * the browser renders that type inline and that the chosen strategy contains it.
 */
export const DISPLAYABLE_DOCUMENT_TYPES: Record<string, DisplayableDocumentType> = {
  'application/pdf': {
    mimeType: 'application/pdf',
    strategy: 'object',
    signature: [0x25, 0x50, 0x44, 0x46, 0x2d], // "%PDF-"
    signatureSearchLimit: PDF_SIGNATURE_SEARCH_LIMIT,
  },
  'text/html': {
    mimeType: 'text/html',
    strategy: 'iframe',
    sandbox: '',
  },
  'application/xhtml+xml': {
    mimeType: 'application/xhtml+xml',
    strategy: 'iframe',
    sandbox: '',
  },
  'text/xml': {
    mimeType: 'text/xml',
    strategy: 'iframe',
    sandbox: '',
  },
  'application/xml': {
    mimeType: 'application/xml',
    strategy: 'iframe',
    sandbox: '',
  },
  'text/plain': {
    mimeType: 'text/plain',
    strategy: 'iframe',
    sandbox: '',
  },
};
 
/**
 * Non-standard spellings seen in real instances, folded onto their canonical
 * entry. Accepting an alias is safe because the canonical entry decides both
 * the Blob type and the signature the payload has to satisfy.
 */
export const DOCUMENT_MIME_TYPE_ALIASES: Record<string, string> = {
  'application/html': 'text/html',
  'application/x-pdf': 'application/pdf',
  'application/acrobat': 'application/pdf',
  'text/pdf': 'application/pdf',
  'application/xhtml': 'application/xhtml+xml',
};
 
/**
 * Lower-cases and strips any parameters (`text/html; charset=utf-8`).
 */
export function normalizeDocumentMimeType(rawMimeType?: string): string | undefined {
  Iif (typeof rawMimeType !== 'string') {
    return undefined;
  }
 
  const normalized = rawMimeType.split(';')[0].trim().toLowerCase();
 
  return normalized || undefined;
}
 
/**
 * Resolves a declared MIME type to its allowlist entry, or undefined when the
 * type is not one this extension will display.
 */
export function getDisplayableDocumentType(
  rawMimeType?: string
): DisplayableDocumentType | undefined {
  const normalized = normalizeDocumentMimeType(rawMimeType);
 
  Iif (!normalized) {
    return undefined;
  }
 
  const canonical = DOCUMENT_MIME_TYPE_ALIASES[normalized] ?? normalized;
 
  return DISPLAYABLE_DOCUMENT_TYPES[canonical];
}
 
/**
 * Checks a payload against its type's magic number, allowing the signature to
 * start anywhere within `signatureSearchLimit` bytes of the payload. Types that
 * have no reliable signature pass, since for those the sandbox rather than the
 * content check is what contains the document.
 */
export function matchesDocumentSignature(
  documentType: DisplayableDocumentType,
  payload: ArrayBuffer
): boolean {
  const { signature, signatureSearchLimit = 0 } = documentType;
 
  Iif (!signature?.length) {
    return true;
  }
 
  Iif (payload.byteLength < signature.length) {
    return false;
  }
 
  // Only the window the signature could still start in needs reading, and it is
  // bounded, so an oversized document costs the same as a small one.
  const lastStart = Math.min(signatureSearchLimit, payload.byteLength - signature.length);
  const head = new Uint8Array(payload, 0, lastStart + signature.length);
 
  for (let start = 0; start <= lastStart; start++) {
    Iif (signature.every((byte, index) => head[start + index] === byte)) {
      return true;
    }
  }
 
  return false;
}