All files parser.js

94.54% Statements 104/110
84.61% Branches 55/65
100% Functions 19/19
96.84% Lines 92/95

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  1x 1x 1x   1x 1x 1x   16x 28x 16x       16x 16x 16x 16x   16x 16x 16x 16x 1x   15x 15x   15x         15x 15x 15x 44x 29x 1x 1x   28x 28x   43x   15x       44x 7592x 7268x   324x   116x 28x   88x   208x 15x     1x       15x         28x   15x 28x 28x 15x           15x 28x 28x 28x   15x 15x 15x   45x 45x 45x 45x   45x 45x   29x 29x     28x   28x 28x 28x 28x   28x 28x     28x   24x   28x   28x                 15x 28x   15x       28x               28x 28x 28x 28x   84x   28x 28x 88x 88x 28x                   28x 24x   4x       28x      
// eslint-disable-next-line import/no-unresolved
const filenamify = require('filenamify/browser');
const bodyDecoder = require('./decoder');
const linkReplacer = require('./link-replacer');
 
const CR = '\r'.charCodeAt(0);
const LF = '\n'.charCodeAt(0);
module.exports = class Parser {
  constructor(config = {}) {
    this.maxFileSize = config.maxFileSize || 50 * 1000 * 1000;
    this.rewriteFn = config.rewriteFn || ((name) => filenamify(name));
    this.gotString = false;
  }
 
  parse(file) { // file is contents
    this.gotString = false;
    Eif (!Buffer.isBuffer(file)) {
      file = Buffer.from(file);
      this.gotString = true;
    }
    const endOfHeaders = Parser.findDoubleCrLf(file);
    const header = file.slice(0, endOfHeaders).toString();
    const separatorMatch = /boundary="(.*)"/g.exec(header);
    if (!separatorMatch) {
      throw new Error('No separator');
    }
    const separator = `--${separatorMatch[1]}`;
    this.parts = Parser.splitByBoundary(file, separator, endOfHeaders + 1, this.maxFileSize)
      .map(Parser.parsePart);
    return this;
  }
 
  static splitByBoundary(file, separator, fromPosition, maxFileSize) {
    let index;
    const sepLen = separator.length;
    const parts = [];
    while ((index = file.indexOf(separator, fromPosition + 1)) !== -1) {
      if (index - fromPosition > 12) { // push non empty parts added sometimes
        if (index - fromPosition > maxFileSize) {
          fromPosition = index;
          continue; // ignore too big chunks
        }
        const part = file.slice(fromPosition + sepLen, index);
        parts.push(part);
      }
      fromPosition = index;
    }
    return parts;
  }
 
  static findDoubleCrLf(file) {
    for (let i = 0, len = file.length; i < len; i++) {
      if (file[i] !== CR && file[i] !== LF) {
        continue;
      }
      if (file[i] === CR && file[i + 1] === LF) {
        // \r\n
        if (file[i + 2] === CR && file[i + 3] === LF) {
          return i;
        }
        continue;
      }
      if (file[i] === LF && file[i + 1] === LF) {
        return i;
      }
    }
    return -1;
  }
 
  rewrite() {
    const replacerMap = {
      'text/html': linkReplacer.html,
      'text/css': linkReplacer.css,
      'image/svg+xml': linkReplacer.svg,
    };
    const getReplacer = (type) => replacerMap[type] || ((body) => body);
 
    const entries = this.parts
      .filter((part) => part.location)
      .map((part) => [part.location.trim(), part]);
    const partMap = new Map();
    /**
     * some MHTML file may have same location with different id.
     * generally leaning forward may be more important.
     * so this action may ignore same location following.
     */
    for (const [location, part] of entries) {
      Eif (!partMap.has(location)) partMap.set(location, part);
      const id = `cid:${part.id}`;
      if (part.id && !partMap.has(id)) partMap.set(id, part);
    }
    const rewriteMap = new Map();
    const urlQueue = []; // record recursive rewrite url, void repeated rewrite
    const proxyRewriteMap = new Proxy(rewriteMap, {
      get: (target, property) => {
        Eif (property === 'get') {
          return (key) => {
            Iif (!key) return;
            Iif (urlQueue.includes(key)) return;
 
            const value = target.get(key);
            if (value !== undefined) return value;
 
            const part = partMap.get(key);
            if (!part) return;
 
            // console.log('key', key);
            urlQueue.push(key);
 
            const replacer = getReplacer(part.type);
            part.body = replacer(part.body, proxyRewriteMap, part.location);
            const url = this.rewriteFn(part.location, part);
            part.rewriteLocation = url;
 
            target.set(part.location, url);
            const id = `cid:${part.id}`;
            // use cid to reference resource
            // eslint-disable-next-line max-len
            if (key === id && part.location && target.get(part.location))target.set(part.location, url);
            // use location to reference resource
            else Eif (key !== id) target.set(id, url);
 
            urlQueue.pop();
 
            return url;
          };
        }
        const value = target[property];
        if (Object.prototype.toString.call(value) === '[object Function]') return value.bind(target);
        return value;
      }
    });
 
    for (const part of this.parts) {
      proxyRewriteMap.get(part.location);
    }
    return this;
  }
 
  spit() {
    return this.parts.map((part) => ({
      filename: part.rewriteLocation,
      content: this.gotString ? part.body.toString() : part.body,
      type: part.type,
    }));
  }
 
  static parsePart(part) {
    const headerEnd = Parser.findDoubleCrLf(part);
    const headerPart = part.slice(0, headerEnd).toString().trim();
    let startBody = headerEnd + 1;
    while ((part[startBody] === CR || part[startBody] === LF)
     && (startBody < headerEnd + 10)) { // remove some initial whitespace
      startBody++;
    }
    const body = part.slice(startBody);
    const headers = new Map(headerPart.split(/\r?\n/g)
      .map((header) => header.split(': '))
      .map(([key, value]) => [key.toLowerCase(), value]));
    return {
      location: headers.get('content-location'),
      id: Parser.parseContentId(headers.get('content-id')),
      type: headers.get('content-type'),
      encoding: (headers.get('content-transfer-encoding') || '').toLowerCase(),
      body: Parser.parseBody(headers.get('content-transfer-encoding'), body),
    };
  }
 
  static parseContentId(contentId) {
    if (!contentId) {
      return undefined;
    }
    return contentId.substring(1, contentId.length - 1);
  }
 
  static parseBody(encoding, body) {
    return bodyDecoder(encoding, body);
  }
};