| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 | "use strict";const punycode = require("punycode");const regexes = require("./lib/regexes.js");const mappingTable = require("./lib/mappingTable.json");const { STATUS_MAPPING } = require("./lib/statusMapping.js");function containsNonASCII(str) {  return /[^\x00-\x7F]/u.test(str);}function findStatus(val, { useSTD3ASCIIRules }) {  let start = 0;  let end = mappingTable.length - 1;  while (start <= end) {    const mid = Math.floor((start + end) / 2);    const target = mappingTable[mid];    const min = Array.isArray(target[0]) ? target[0][0] : target[0];    const max = Array.isArray(target[0]) ? target[0][1] : target[0];    if (min <= val && max >= val) {      if (useSTD3ASCIIRules &&          (target[1] === STATUS_MAPPING.disallowed_STD3_valid || target[1] === STATUS_MAPPING.disallowed_STD3_mapped)) {        return [STATUS_MAPPING.disallowed, ...target.slice(2)];      } else if (target[1] === STATUS_MAPPING.disallowed_STD3_valid) {        return [STATUS_MAPPING.valid, ...target.slice(2)];      } else if (target[1] === STATUS_MAPPING.disallowed_STD3_mapped) {        return [STATUS_MAPPING.mapped, ...target.slice(2)];      }      return target.slice(1);    } else if (min > val) {      end = mid - 1;    } else {      start = mid + 1;    }  }  return null;}function mapChars(domainName, { useSTD3ASCIIRules, processingOption }) {  let hasError = false;  let processed = "";  for (const ch of domainName) {    const [status, mapping] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });    switch (status) {      case STATUS_MAPPING.disallowed:        hasError = true;        processed += ch;        break;      case STATUS_MAPPING.ignored:        break;      case STATUS_MAPPING.mapped:        processed += mapping;        break;      case STATUS_MAPPING.deviation:        if (processingOption === "transitional") {          processed += mapping;        } else {          processed += ch;        }        break;      case STATUS_MAPPING.valid:        processed += ch;        break;    }  }  return {    string: processed,    error: hasError  };}function validateLabel(label, { checkHyphens, checkBidi, checkJoiners, processingOption, useSTD3ASCIIRules }) {  if (label.normalize("NFC") !== label) {    return false;  }  const codePoints = Array.from(label);  if (checkHyphens) {    if ((codePoints[2] === "-" && codePoints[3] === "-") ||        (label.startsWith("-") || label.endsWith("-"))) {      return false;    }  }  if (label.includes(".") ||      (codePoints.length > 0 && regexes.combiningMarks.test(codePoints[0]))) {    return false;  }  for (const ch of codePoints) {    const [status] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });    if ((processingOption === "transitional" && status !== STATUS_MAPPING.valid) ||        (processingOption === "nontransitional" &&         status !== STATUS_MAPPING.valid && status !== STATUS_MAPPING.deviation)) {      return false;    }  }  // https://tools.ietf.org/html/rfc5892#appendix-A  if (checkJoiners) {    let last = 0;    for (const [i, ch] of codePoints.entries()) {      if (ch === "\u200C" || ch === "\u200D") {        if (i > 0) {          if (regexes.combiningClassVirama.test(codePoints[i - 1])) {            continue;          }          if (ch === "\u200C") {            // TODO: make this more efficient            const next = codePoints.indexOf("\u200C", i + 1);            const test = next < 0 ? codePoints.slice(last) : codePoints.slice(last, next);            if (regexes.validZWNJ.test(test.join(""))) {              last = i + 1;              continue;            }          }        }        return false;      }    }  }  // https://tools.ietf.org/html/rfc5893#section-2  if (checkBidi) {    let rtl;    // 1    if (regexes.bidiS1LTR.test(codePoints[0])) {      rtl = false;    } else if (regexes.bidiS1RTL.test(codePoints[0])) {      rtl = true;    } else {      return false;    }    if (rtl) {      // 2-4      if (!regexes.bidiS2.test(label) ||          !regexes.bidiS3.test(label) ||          (regexes.bidiS4EN.test(label) && regexes.bidiS4AN.test(label))) {        return false;      }    } else if (!regexes.bidiS5.test(label) ||               !regexes.bidiS6.test(label)) { // 5-6      return false;    }  }  return true;}function isBidiDomain(labels) {  const domain = labels.map(label => {    if (label.startsWith("xn--")) {      try {        return punycode.decode(label.substring(4));      } catch (err) {        return "";      }    }    return label;  }).join(".");  return regexes.bidiDomain.test(domain);}function processing(domainName, options) {  const { processingOption } = options;  // 1. Map.  let { string, error } = mapChars(domainName, options);  // 2. Normalize.  string = string.normalize("NFC");  // 3. Break.  const labels = string.split(".");  const isBidi = isBidiDomain(labels);  // 4. Convert/Validate.  for (const [i, origLabel] of labels.entries()) {    let label = origLabel;    let curProcessing = processingOption;    if (label.startsWith("xn--")) {      try {        label = punycode.decode(label.substring(4));        labels[i] = label;      } catch (err) {        error = true;        continue;      }      curProcessing = "nontransitional";    }    // No need to validate if we already know there is an error.    if (error) {      continue;    }    const validation = validateLabel(label, {      ...options,      processingOption: curProcessing,      checkBidi: options.checkBidi && isBidi    });    if (!validation) {      error = true;    }  }  return {    string: labels.join("."),    error  };}function toASCII(domainName, {  checkHyphens = false,  checkBidi = false,  checkJoiners = false,  useSTD3ASCIIRules = false,  processingOption = "nontransitional",  verifyDNSLength = false} = {}) {  if (processingOption !== "transitional" && processingOption !== "nontransitional") {    throw new RangeError("processingOption must be either transitional or nontransitional");  }  const result = processing(domainName, {    processingOption,    checkHyphens,    checkBidi,    checkJoiners,    useSTD3ASCIIRules  });  let labels = result.string.split(".");  labels = labels.map(l => {    if (containsNonASCII(l)) {      try {        return `xn--${punycode.encode(l)}`;      } catch (e) {        result.error = true;      }    }    return l;  });  if (verifyDNSLength) {    const total = labels.join(".").length;    if (total > 253 || total === 0) {      result.error = true;    }    for (let i = 0; i < labels.length; ++i) {      if (labels[i].length > 63 || labels[i].length === 0) {        result.error = true;        break;      }    }  }  if (result.error) {    return null;  }  return labels.join(".");}function toUnicode(domainName, {  checkHyphens = false,  checkBidi = false,  checkJoiners = false,  useSTD3ASCIIRules = false,  processingOption = "nontransitional"} = {}) {  const result = processing(domainName, {    processingOption,    checkHyphens,    checkBidi,    checkJoiners,    useSTD3ASCIIRules  });  return {    domain: result.string,    error: result.error  };}module.exports = {  toASCII,  toUnicode};
 |