summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/CSV.js
blob: babf1c58c99bca1e20c5eb5f5ded19e9ca215df3 (plain)
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
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * A Class to parse CSV files
 */

"use strict";

const EXPORTED_SYMBOLS = ["CSV"];

const QUOTATION_MARK = '"';
const LINE_BREAKS = ["\r", "\n"];
const EOL = {};

class ParsingFailedException extends Error {
  constructor(message) {
    super(message ? message : `Stopped parsing because of wrong csv format`);
  }
}

class CSV {
  /**
   * Parses a csv formated string into rows split into [headerLine, parsedLines].
   * The csv string format has to follow RFC 4180, otherwise the parsing process is stopped and a ParsingFailedException is thrown, e.g.:
   * (wrong format => right format):
   * 'abc"def' => 'abc""def'
   * abc,def => "abc,def"
   *
   * @param {string} text
   * @param {string} delimiter a comma for CSV files and a tab for TSV files
   * @returns {Array[]} headerLine: column names (first line of text), parsedLines: Array of Login Objects with column name as properties and login data as values.
   */
  static parse(text, delimiter) {
    let headerline = [];
    let parsedLines = [];

    for (let row of this.mapValuesToRows(this.readCSV(text, delimiter))) {
      if (!headerline.length) {
        headerline = row;
      } else {
        let login = {};
        row.forEach((attr, i) => (login[headerline[i]] = attr));
        parsedLines.push(login);
      }
    }
    return [headerline, parsedLines];
  }
  static *readCSV(text, delimiter) {
    function maySkipMultipleLineBreaks() {
      while (LINE_BREAKS.includes(text[current])) {
        current++;
      }
    }
    function readUntilSingleQuote() {
      const start = ++current;
      while (current < text.length) {
        if (text[current] === QUOTATION_MARK) {
          if (text[current + 1] !== QUOTATION_MARK) {
            const result = text.slice(start, current).replaceAll('""', '"');
            current++;
            return result;
          }
          current++;
        }
        current++;
      }
      throw new ParsingFailedException();
    }
    function readUntilDelimiterOrNewLine() {
      const start = current;
      while (current < text.length) {
        if (text[current] === delimiter) {
          const result = text.slice(start, current);
          current++;
          return result;
        } else if (LINE_BREAKS.includes(text[current])) {
          const result = text.slice(start, current);
          return result;
        }
        current++;
      }
      return text.slice(start);
    }
    let current = 0;
    maySkipMultipleLineBreaks();

    while (current < text.length) {
      if (LINE_BREAKS.includes(text[current])) {
        maySkipMultipleLineBreaks();
        yield EOL;
      }

      let quotedValue = "";
      let value = "";

      if (text[current] === QUOTATION_MARK) {
        quotedValue = readUntilSingleQuote();
      }

      value = readUntilDelimiterOrNewLine();

      if (quotedValue && value) {
        throw new ParsingFailedException();
      }

      yield quotedValue ? quotedValue : value;
    }
  }

  static *mapValuesToRows(values) {
    let row = [];
    for (const value of values) {
      if (value === EOL) {
        yield row;
        row = [];
      } else {
        row.push(value);
      }
    }
    if (!(row.length === 1 && row[0] === "") && row.length) {
      yield row;
    }
  }
}