All files parser.js

98.67% Statements 74/75
84.78% Branches 39/46
100% Functions 16/16
98.59% Lines 70/71

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 1371x 1x 1x   1x 1x 1x   16x 16x 16x       16x 16x 16x 16x     16x 16x 1x     15x 15x 15x       15x 15x   15x     15x         15x 15x 15x 29x 29x 1x 1x   28x 28x   28x   15x       44x 7592x 7268x   324x   116x 28x   88x   208x 15x     1x       15x 28x 28x 15x 28x 4x   15x 28x       9x 28x   15x       28x               28x 28x 28x 28x   84x   28x 28x 88x 88x 28x                   28x 24x   4x       28x      
const filenamify = require('filenamify');
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 || filenamify;
    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);
    if (endOfHeaders < 0) {
      throw new Error('No double cr\\lf');
    }
 
    const header = file.slice(0, endOfHeaders).toString();
    const separatorMatch = /boundary="(.*)"/g.exec(header);
    Iif (!separatorMatch) {
      throw new Error('No separator');
    }
 
    const separator = `--${separatorMatch[1]}`;
    const startOfFirstPart = file.indexOf(separator, endOfHeaders + 1);
 
    this.parts = Parser.splitByBoundary(file, separator, startOfFirstPart, 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) {
      Eif (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 entries = this.parts
      .filter(part => part.location)
      .map(part => [part.location.trim(), this.rewriteFn(part.location, part).trim()]);
    const rewriteMap = new Map(entries);
    for (const part of this.parts.filter(x => x.id)) {
      rewriteMap.set(`cid:${part.id}`, this.rewriteFn(part.location || part.id.trim(), part));
    }
    for (const part of this.parts) {
      const replacer = ({
        'text/html': linkReplacer.html,
        'text/css': linkReplacer.css,
        'image/svg+xml': linkReplacer.svg,
      })[part.type] || (body => body);
      part.body = replacer(part.body, rewriteMap, part.location);
    }
    return this;
  }
 
  spit() {
    return this.parts.map(part => ({
      filename: this.rewriteFn(part.location || part.id, part),
      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);
  }
};