Files
securebit-chat/node_modules/qrcode/lib/core/segments.js
lockbitchat 0f8399ec88 feat(security,ui): self-host React deps, Tailwind, fonts; strict CSP; local QR; better selection state
Replace CDN React/ReactDOM/Babel with local libs; remove Babel and inline scripts
Build Tailwind locally, add safelist; switch to assets/tailwind.css
Self-host Font Awesome and Inter (CSS + woff2); remove external font CDNs
Implement strict CSP (no unsafe-inline/eval; scripts/styles/fonts from self)
Extract inline handlers; move PWA scripts to external files
Add local QR code generation (qrcode lib) and remove api.qrserver.com
Improve SessionTypeSelector visual selection (highlighted background and ring)
Keep PWA working with service worker and offline assets
Refs: CSP hardening, offline-first, no external dependencies
2025-09-08 16:04:58 -04:00

331 lines
9.1 KiB
JavaScript

const Mode = require('./mode')
const NumericData = require('./numeric-data')
const AlphanumericData = require('./alphanumeric-data')
const ByteData = require('./byte-data')
const KanjiData = require('./kanji-data')
const Regex = require('./regex')
const Utils = require('./utils')
const dijkstra = require('dijkstrajs')
/**
* Returns UTF8 byte length
*
* @param {String} str Input string
* @return {Number} Number of byte
*/
function getStringByteLength (str) {
return unescape(encodeURIComponent(str)).length
}
/**
* Get a list of segments of the specified mode
* from a string
*
* @param {Mode} mode Segment mode
* @param {String} str String to process
* @return {Array} Array of object with segments data
*/
function getSegments (regex, mode, str) {
const segments = []
let result
while ((result = regex.exec(str)) !== null) {
segments.push({
data: result[0],
index: result.index,
mode: mode,
length: result[0].length
})
}
return segments
}
/**
* Extracts a series of segments with the appropriate
* modes from a string
*
* @param {String} dataStr Input string
* @return {Array} Array of object with segments data
*/
function getSegmentsFromString (dataStr) {
const numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr)
const alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr)
let byteSegs
let kanjiSegs
if (Utils.isKanjiModeEnabled()) {
byteSegs = getSegments(Regex.BYTE, Mode.BYTE, dataStr)
kanjiSegs = getSegments(Regex.KANJI, Mode.KANJI, dataStr)
} else {
byteSegs = getSegments(Regex.BYTE_KANJI, Mode.BYTE, dataStr)
kanjiSegs = []
}
const segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs)
return segs
.sort(function (s1, s2) {
return s1.index - s2.index
})
.map(function (obj) {
return {
data: obj.data,
mode: obj.mode,
length: obj.length
}
})
}
/**
* Returns how many bits are needed to encode a string of
* specified length with the specified mode
*
* @param {Number} length String length
* @param {Mode} mode Segment mode
* @return {Number} Bit length
*/
function getSegmentBitsLength (length, mode) {
switch (mode) {
case Mode.NUMERIC:
return NumericData.getBitsLength(length)
case Mode.ALPHANUMERIC:
return AlphanumericData.getBitsLength(length)
case Mode.KANJI:
return KanjiData.getBitsLength(length)
case Mode.BYTE:
return ByteData.getBitsLength(length)
}
}
/**
* Merges adjacent segments which have the same mode
*
* @param {Array} segs Array of object with segments data
* @return {Array} Array of object with segments data
*/
function mergeSegments (segs) {
return segs.reduce(function (acc, curr) {
const prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null
if (prevSeg && prevSeg.mode === curr.mode) {
acc[acc.length - 1].data += curr.data
return acc
}
acc.push(curr)
return acc
}, [])
}
/**
* Generates a list of all possible nodes combination which
* will be used to build a segments graph.
*
* Nodes are divided by groups. Each group will contain a list of all the modes
* in which is possible to encode the given text.
*
* For example the text '12345' can be encoded as Numeric, Alphanumeric or Byte.
* The group for '12345' will contain then 3 objects, one for each
* possible encoding mode.
*
* Each node represents a possible segment.
*
* @param {Array} segs Array of object with segments data
* @return {Array} Array of object with segments data
*/
function buildNodes (segs) {
const nodes = []
for (let i = 0; i < segs.length; i++) {
const seg = segs[i]
switch (seg.mode) {
case Mode.NUMERIC:
nodes.push([seg,
{ data: seg.data, mode: Mode.ALPHANUMERIC, length: seg.length },
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
])
break
case Mode.ALPHANUMERIC:
nodes.push([seg,
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
])
break
case Mode.KANJI:
nodes.push([seg,
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
])
break
case Mode.BYTE:
nodes.push([
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
])
}
}
return nodes
}
/**
* Builds a graph from a list of nodes.
* All segments in each node group will be connected with all the segments of
* the next group and so on.
*
* At each connection will be assigned a weight depending on the
* segment's byte length.
*
* @param {Array} nodes Array of object with segments data
* @param {Number} version QR Code version
* @return {Object} Graph of all possible segments
*/
function buildGraph (nodes, version) {
const table = {}
const graph = { start: {} }
let prevNodeIds = ['start']
for (let i = 0; i < nodes.length; i++) {
const nodeGroup = nodes[i]
const currentNodeIds = []
for (let j = 0; j < nodeGroup.length; j++) {
const node = nodeGroup[j]
const key = '' + i + j
currentNodeIds.push(key)
table[key] = { node: node, lastCount: 0 }
graph[key] = {}
for (let n = 0; n < prevNodeIds.length; n++) {
const prevNodeId = prevNodeIds[n]
if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) {
graph[prevNodeId][key] =
getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) -
getSegmentBitsLength(table[prevNodeId].lastCount, node.mode)
table[prevNodeId].lastCount += node.length
} else {
if (table[prevNodeId]) table[prevNodeId].lastCount = node.length
graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) +
4 + Mode.getCharCountIndicator(node.mode, version) // switch cost
}
}
}
prevNodeIds = currentNodeIds
}
for (let n = 0; n < prevNodeIds.length; n++) {
graph[prevNodeIds[n]].end = 0
}
return { map: graph, table: table }
}
/**
* Builds a segment from a specified data and mode.
* If a mode is not specified, the more suitable will be used.
*
* @param {String} data Input data
* @param {Mode | String} modesHint Data mode
* @return {Segment} Segment
*/
function buildSingleSegment (data, modesHint) {
let mode
const bestMode = Mode.getBestModeForData(data)
mode = Mode.from(modesHint, bestMode)
// Make sure data can be encoded
if (mode !== Mode.BYTE && mode.bit < bestMode.bit) {
throw new Error('"' + data + '"' +
' cannot be encoded with mode ' + Mode.toString(mode) +
'.\n Suggested mode is: ' + Mode.toString(bestMode))
}
// Use Mode.BYTE if Kanji support is disabled
if (mode === Mode.KANJI && !Utils.isKanjiModeEnabled()) {
mode = Mode.BYTE
}
switch (mode) {
case Mode.NUMERIC:
return new NumericData(data)
case Mode.ALPHANUMERIC:
return new AlphanumericData(data)
case Mode.KANJI:
return new KanjiData(data)
case Mode.BYTE:
return new ByteData(data)
}
}
/**
* Builds a list of segments from an array.
* Array can contain Strings or Objects with segment's info.
*
* For each item which is a string, will be generated a segment with the given
* string and the more appropriate encoding mode.
*
* For each item which is an object, will be generated a segment with the given
* data and mode.
* Objects must contain at least the property "data".
* If property "mode" is not present, the more suitable mode will be used.
*
* @param {Array} array Array of objects with segments data
* @return {Array} Array of Segments
*/
exports.fromArray = function fromArray (array) {
return array.reduce(function (acc, seg) {
if (typeof seg === 'string') {
acc.push(buildSingleSegment(seg, null))
} else if (seg.data) {
acc.push(buildSingleSegment(seg.data, seg.mode))
}
return acc
}, [])
}
/**
* Builds an optimized sequence of segments from a string,
* which will produce the shortest possible bitstream.
*
* @param {String} data Input string
* @param {Number} version QR Code version
* @return {Array} Array of segments
*/
exports.fromString = function fromString (data, version) {
const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled())
const nodes = buildNodes(segs)
const graph = buildGraph(nodes, version)
const path = dijkstra.find_path(graph.map, 'start', 'end')
const optimizedSegs = []
for (let i = 1; i < path.length - 1; i++) {
optimizedSegs.push(graph.table[path[i]].node)
}
return exports.fromArray(mergeSegments(optimizedSegs))
}
/**
* Splits a string in various segments with the modes which
* best represent their content.
* The produced segments are far from being optimized.
* The output of this function is only used to estimate a QR Code version
* which may contain the data.
*
* @param {string} data Input string
* @return {Array} Array of segments
*/
exports.rawSplit = function rawSplit (data) {
return exports.fromArray(
getSegmentsFromString(data, Utils.isKanjiModeEnabled())
)
}