592 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			592 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| //  SPDX-License-Identifier: LGPL-2.1-or-later
 | |
| //  Copyright (c) 2015-2024 MariaDB Corporation Ab
 | |
| 
 | |
| 'use strict';
 | |
| 
 | |
| const Errors = require('../misc/errors');
 | |
| 
 | |
| /**
 | |
|  * Object to easily parse buffer.
 | |
|  * Packet are MUTABLE (buffer are changed, to avoid massive packet object creation).
 | |
|  * Use clone() in case immutability is required
 | |
|  *
 | |
|  */
 | |
| class Packet {
 | |
|   update(buf, pos, end) {
 | |
|     this.buf = buf;
 | |
|     this.pos = pos;
 | |
|     this.end = end;
 | |
|     return this;
 | |
|   }
 | |
| 
 | |
|   skip(n) {
 | |
|     this.pos += n;
 | |
|   }
 | |
| 
 | |
|   readGeometry(defaultVal) {
 | |
|     const geoBuf = this.readBufferLengthEncoded();
 | |
|     if (geoBuf === null || geoBuf.length === 0) {
 | |
|       return defaultVal;
 | |
|     }
 | |
|     let geoPos = 4;
 | |
|     return readGeometryObject(false);
 | |
| 
 | |
|     function parseCoordinates(byteOrder) {
 | |
|       geoPos += 16;
 | |
|       const x = byteOrder ? geoBuf.readDoubleLE(geoPos - 16) : geoBuf.readDoubleBE(geoPos - 16);
 | |
|       const y = byteOrder ? geoBuf.readDoubleLE(geoPos - 8) : geoBuf.readDoubleBE(geoPos - 8);
 | |
|       return [x, y];
 | |
|     }
 | |
| 
 | |
|     function readGeometryObject(inner) {
 | |
|       const byteOrder = geoBuf[geoPos++];
 | |
|       const wkbType = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
 | |
|       geoPos += 4;
 | |
|       switch (wkbType) {
 | |
|         case 1: //wkbPoint
 | |
|           const coords = parseCoordinates(byteOrder);
 | |
| 
 | |
|           if (inner) return coords;
 | |
|           return {
 | |
|             type: 'Point',
 | |
|             coordinates: coords
 | |
|           };
 | |
| 
 | |
|         case 2: //wkbLineString
 | |
|           const pointNumber = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
 | |
|           geoPos += 4;
 | |
|           let coordinates = [];
 | |
|           for (let i = 0; i < pointNumber; i++) {
 | |
|             coordinates.push(parseCoordinates(byteOrder));
 | |
|           }
 | |
|           if (inner) return coordinates;
 | |
|           return {
 | |
|             type: 'LineString',
 | |
|             coordinates: coordinates
 | |
|           };
 | |
| 
 | |
|         case 3: //wkbPolygon
 | |
|           let polygonCoordinates = [];
 | |
|           const numRings = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
 | |
|           geoPos += 4;
 | |
|           for (let ring = 0; ring < numRings; ring++) {
 | |
|             const pointNumber = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
 | |
|             geoPos += 4;
 | |
|             let linesCoordinates = [];
 | |
|             for (let i = 0; i < pointNumber; i++) {
 | |
|               linesCoordinates.push(parseCoordinates(byteOrder));
 | |
|             }
 | |
|             polygonCoordinates.push(linesCoordinates);
 | |
|           }
 | |
| 
 | |
|           if (inner) return polygonCoordinates;
 | |
|           return {
 | |
|             type: 'Polygon',
 | |
|             coordinates: polygonCoordinates
 | |
|           };
 | |
| 
 | |
|         case 4: //wkbMultiPoint
 | |
|           return {
 | |
|             type: 'MultiPoint',
 | |
|             coordinates: parseGeomArray(byteOrder, true)
 | |
|           };
 | |
| 
 | |
|         case 5: //wkbMultiLineString
 | |
|           return {
 | |
|             type: 'MultiLineString',
 | |
|             coordinates: parseGeomArray(byteOrder, true)
 | |
|           };
 | |
|         case 6: //wkbMultiPolygon
 | |
|           return {
 | |
|             type: 'MultiPolygon',
 | |
|             coordinates: parseGeomArray(byteOrder, true)
 | |
|           };
 | |
|         case 7: //wkbGeometryCollection
 | |
|           return {
 | |
|             type: 'GeometryCollection',
 | |
|             geometries: parseGeomArray(byteOrder, false)
 | |
|           };
 | |
|       }
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function parseGeomArray(byteOrder, inner) {
 | |
|       let coordinates = [];
 | |
|       const number = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
 | |
|       geoPos += 4;
 | |
|       for (let i = 0; i < number; i++) {
 | |
|         coordinates.push(readGeometryObject(inner));
 | |
|       }
 | |
|       return coordinates;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   peek() {
 | |
|     return this.buf[this.pos];
 | |
|   }
 | |
| 
 | |
|   remaining() {
 | |
|     return this.end - this.pos > 0;
 | |
|   }
 | |
| 
 | |
|   readInt8() {
 | |
|     const val = this.buf[this.pos++];
 | |
|     return val | ((val & (2 ** 7)) * 0x1fffffe);
 | |
|   }
 | |
| 
 | |
|   readUInt8() {
 | |
|     return this.buf[this.pos++];
 | |
|   }
 | |
| 
 | |
|   readInt16() {
 | |
|     const first = this.buf[this.pos++];
 | |
|     const last = this.buf[this.pos++];
 | |
|     const val = first + last * 2 ** 8;
 | |
|     return val | ((val & (2 ** 15)) * 0x1fffe);
 | |
|   }
 | |
| 
 | |
|   readUInt16() {
 | |
|     return this.buf[this.pos++] + this.buf[this.pos++] * 2 ** 8;
 | |
|   }
 | |
| 
 | |
|   readInt24() {
 | |
|     const first = this.buf[this.pos];
 | |
|     const last = this.buf[this.pos + 2];
 | |
|     const val = first + this.buf[this.pos + 1] * 2 ** 8 + last * 2 ** 16;
 | |
|     this.pos += 3;
 | |
|     return val | ((val & (2 ** 23)) * 0x1fe);
 | |
|   }
 | |
| 
 | |
|   readUInt24() {
 | |
|     return this.buf[this.pos++] + this.buf[this.pos++] * 2 ** 8 + this.buf[this.pos++] * 2 ** 16;
 | |
|   }
 | |
| 
 | |
|   readUInt32() {
 | |
|     return (
 | |
|       this.buf[this.pos++] +
 | |
|       this.buf[this.pos++] * 2 ** 8 +
 | |
|       this.buf[this.pos++] * 2 ** 16 +
 | |
|       this.buf[this.pos++] * 2 ** 24
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   readInt32() {
 | |
|     return (
 | |
|       this.buf[this.pos++] +
 | |
|       this.buf[this.pos++] * 2 ** 8 +
 | |
|       this.buf[this.pos++] * 2 ** 16 +
 | |
|       (this.buf[this.pos++] << 24)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   readBigInt64() {
 | |
|     const val = this.buf.readBigInt64LE(this.pos);
 | |
|     this.pos += 8;
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   readBigUInt64() {
 | |
|     const val = this.buf.readBigUInt64LE(this.pos);
 | |
|     this.pos += 8;
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Metadata are length encoded, but cannot have length > 256, so simplified readUnsignedLength
 | |
|    * @returns {number}
 | |
|    */
 | |
|   readMetadataLength() {
 | |
|     const type = this.buf[this.pos++];
 | |
|     if (type < 0xfb) return type;
 | |
|     return this.readUInt16();
 | |
|   }
 | |
| 
 | |
|   readUnsignedLength() {
 | |
|     const type = this.buf[this.pos++];
 | |
|     if (type < 0xfb) return type;
 | |
|     switch (type) {
 | |
|       case 0xfb:
 | |
|         return null;
 | |
|       case 0xfc:
 | |
|         //readUInt16();
 | |
|         return this.buf[this.pos++] + this.buf[this.pos++] * 2 ** 8;
 | |
|       case 0xfd:
 | |
|         //readUInt24();
 | |
|         return this.buf[this.pos++] + this.buf[this.pos++] * 2 ** 8 + this.buf[this.pos++] * 2 ** 16;
 | |
|       case 0xfe:
 | |
|         // limitation to BigInt signed value
 | |
|         return Number(this.readBigInt64());
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   readBuffer(len) {
 | |
|     this.pos += len;
 | |
|     return this.buf.subarray(this.pos - len, this.pos);
 | |
|   }
 | |
| 
 | |
|   readBufferRemaining() {
 | |
|     let b = this.buf.subarray(this.pos, this.end);
 | |
|     this.pos = this.end;
 | |
|     return b;
 | |
|   }
 | |
| 
 | |
|   readBufferLengthEncoded() {
 | |
|     const len = this.readUnsignedLength();
 | |
|     if (len === null) return null;
 | |
|     this.pos += len;
 | |
|     return this.buf.subarray(this.pos - len, this.pos);
 | |
|   }
 | |
| 
 | |
|   readStringNullEnded() {
 | |
|     let initialPosition = this.pos;
 | |
|     let cnt = 0;
 | |
|     while (this.remaining() > 0 && this.buf[this.pos++] !== 0) {
 | |
|       cnt++;
 | |
|     }
 | |
|     return this.buf.toString(undefined, initialPosition, initialPosition + cnt);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Return unsigned Bigint.
 | |
|    *
 | |
|    * Could be used for reading other kind of value than InsertId, if reading possible null value
 | |
|    * @returns {bigint}
 | |
|    */
 | |
|   readInsertId() {
 | |
|     const type = this.buf[this.pos++];
 | |
|     if (type < 0xfb) return BigInt(type);
 | |
|     switch (type) {
 | |
|       case 0xfc:
 | |
|         return BigInt(this.buf[this.pos++] + this.buf[this.pos++] * 2 ** 8);
 | |
|       case 0xfd:
 | |
|         return BigInt(this.buf[this.pos++] + this.buf[this.pos++] * 2 ** 8 + this.buf[this.pos++] * 2 ** 16);
 | |
|       case 0xfe:
 | |
|         return this.readBigInt64();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   readAsciiStringLengthEncoded() {
 | |
|     const len = this.readUnsignedLength();
 | |
|     if (len === null) return null;
 | |
|     this.pos += len;
 | |
|     return this.buf.toString('ascii', this.pos - len, this.pos);
 | |
|   }
 | |
| 
 | |
|   readStringLengthEncoded() {
 | |
|     throw new Error('code is normally superseded by Node encoder or Iconv depending on charset used');
 | |
|   }
 | |
| 
 | |
|   readBigIntLengthEncoded() {
 | |
|     const len = this.buf[this.pos++];
 | |
| 
 | |
|     // fast-path: if length encoded is < to 16, value is in safe integer range, using atoi
 | |
|     if (len < 16) {
 | |
|       return BigInt(this._atoi(len));
 | |
|     }
 | |
| 
 | |
|     if (len === 0xfb) return null;
 | |
| 
 | |
|     return this.readBigIntFromLen(len);
 | |
|   }
 | |
| 
 | |
|   readBigIntFromLen(len) {
 | |
|     // atoll
 | |
|     let result = 0n;
 | |
|     let negate = false;
 | |
|     let begin = this.pos;
 | |
| 
 | |
|     if (len > 0 && this.buf[begin] === 45) {
 | |
|       //minus sign
 | |
|       negate = true;
 | |
|       begin++;
 | |
|     }
 | |
|     for (; begin < this.pos + len; begin++) {
 | |
|       result = result * 10n + BigInt(this.buf[begin] - 48);
 | |
|     }
 | |
|     this.pos += len;
 | |
|     return negate ? -1n * result : result;
 | |
|   }
 | |
| 
 | |
|   readDecimalLengthEncoded() {
 | |
|     const len = this.buf[this.pos++];
 | |
|     if (len === 0xfb) return null;
 | |
|     this.pos += len;
 | |
|     return this.buf.toString('ascii', this.pos - len, this.pos);
 | |
|   }
 | |
| 
 | |
|   readDate() {
 | |
|     const len = this.buf[this.pos++];
 | |
|     if (len === 0xfb) return null;
 | |
|     let res = [];
 | |
|     let value = 0;
 | |
|     let initPos = this.pos;
 | |
|     this.pos += len;
 | |
|     while (initPos < this.pos) {
 | |
|       const char = this.buf[initPos++];
 | |
|       if (char === 45) {
 | |
|         //minus separator
 | |
|         res.push(value);
 | |
|         value = 0;
 | |
|       } else {
 | |
|         value = value * 10 + char - 48;
 | |
|       }
 | |
|     }
 | |
|     res.push(value);
 | |
| 
 | |
|     //handle zero-date as null
 | |
|     if (res[0] === 0 && res[1] === 0 && res[2] === 0) return null;
 | |
| 
 | |
|     return new Date(res[0], res[1] - 1, res[2]);
 | |
|   }
 | |
| 
 | |
|   readBinaryDate(opts) {
 | |
|     const len = this.buf[this.pos++];
 | |
|     let year = 0;
 | |
|     let month = 0;
 | |
|     let day = 0;
 | |
|     if (len > 0) {
 | |
|       year = this.readInt16();
 | |
|       if (len > 2) {
 | |
|         month = this.readUInt8() - 1;
 | |
|         if (len > 3) {
 | |
|           day = this.readUInt8();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     if (year === 0 && month === 0 && day === 0) return opts.dateStrings ? '0000-00-00' : null;
 | |
|     if (opts.dateStrings) {
 | |
|       return `${appendZero(year, 4)}-${appendZero(month + 1, 2)}-${appendZero(day, 2)}`;
 | |
|     }
 | |
|     //handle zero-date as null
 | |
|     return new Date(year, month, day);
 | |
|   }
 | |
| 
 | |
|   readDateTime() {
 | |
|     const len = this.buf[this.pos++];
 | |
|     if (len === 0xfb) return null;
 | |
|     this.pos += len;
 | |
|     const str = this.buf.toString('ascii', this.pos - len, this.pos);
 | |
|     if (str.startsWith('0000-00-00 00:00:00')) return null;
 | |
|     return new Date(str);
 | |
|   }
 | |
| 
 | |
|   readBinaryDateTime() {
 | |
|     const len = this.buf[this.pos++];
 | |
|     let year = 0;
 | |
|     let month = 0;
 | |
|     let day = 0;
 | |
|     let hour = 0;
 | |
|     let min = 0;
 | |
|     let sec = 0;
 | |
|     let microSec = 0;
 | |
| 
 | |
|     if (len > 0) {
 | |
|       year = this.readInt16();
 | |
|       if (len > 2) {
 | |
|         month = this.readUInt8();
 | |
|         if (len > 3) {
 | |
|           day = this.readUInt8();
 | |
|           if (len > 4) {
 | |
|             hour = this.readUInt8();
 | |
|             min = this.readUInt8();
 | |
|             sec = this.readUInt8();
 | |
|             if (len > 7) {
 | |
|               microSec = this.readUInt32();
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     //handle zero-date as null
 | |
|     if (year === 0 && month === 0 && day === 0 && hour === 0 && min === 0 && sec === 0 && microSec === 0) return null;
 | |
|     return new Date(year, month - 1, day, hour, min, sec, microSec / 1000);
 | |
|   }
 | |
| 
 | |
|   readBinaryDateTimeAsString(scale) {
 | |
|     const len = this.buf[this.pos++];
 | |
|     let year = 0;
 | |
|     let month = 0;
 | |
|     let day = 0;
 | |
|     let hour = 0;
 | |
|     let min = 0;
 | |
|     let sec = 0;
 | |
|     let microSec = 0;
 | |
| 
 | |
|     if (len > 0) {
 | |
|       year = this.readInt16();
 | |
|       if (len > 2) {
 | |
|         month = this.readUInt8();
 | |
|         if (len > 3) {
 | |
|           day = this.readUInt8();
 | |
|           if (len > 4) {
 | |
|             hour = this.readUInt8();
 | |
|             min = this.readUInt8();
 | |
|             sec = this.readUInt8();
 | |
|             if (len > 7) {
 | |
|               microSec = this.readUInt32();
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     //handle zero-date as null
 | |
|     if (year === 0 && month === 0 && day === 0 && hour === 0 && min === 0 && sec === 0 && microSec === 0)
 | |
|       return '0000-00-00 00:00:00' + (scale > 0 ? '.000000'.substring(0, scale + 1) : '');
 | |
| 
 | |
|     return (
 | |
|       appendZero(year, 4) +
 | |
|       '-' +
 | |
|       appendZero(month, 2) +
 | |
|       '-' +
 | |
|       appendZero(day, 2) +
 | |
|       ' ' +
 | |
|       appendZero(hour, 2) +
 | |
|       ':' +
 | |
|       appendZero(min, 2) +
 | |
|       ':' +
 | |
|       appendZero(sec, 2) +
 | |
|       (microSec > 0
 | |
|         ? scale > 0
 | |
|           ? '.' + appendZero(microSec, 6).substring(0, scale)
 | |
|           : '.' + appendZero(microSec, 6)
 | |
|         : scale > 0
 | |
|           ? '.' + appendZero(microSec, 6).substring(0, scale)
 | |
|           : '')
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   readBinaryTime() {
 | |
|     const len = this.buf[this.pos++];
 | |
|     let negate = false;
 | |
|     let hour = 0;
 | |
|     let min = 0;
 | |
|     let sec = 0;
 | |
|     let microSec = 0;
 | |
| 
 | |
|     if (len > 0) {
 | |
|       negate = this.buf[this.pos++] === 1;
 | |
|       hour = this.readUInt32() * 24 + this.readUInt8();
 | |
|       min = this.readUInt8();
 | |
|       sec = this.readUInt8();
 | |
|       if (len > 8) {
 | |
|         microSec = this.readUInt32();
 | |
|       }
 | |
|     }
 | |
|     let val = appendZero(hour, 2) + ':' + appendZero(min, 2) + ':' + appendZero(sec, 2);
 | |
|     if (microSec > 0) {
 | |
|       val += '.' + appendZero(microSec, 6);
 | |
|     }
 | |
|     if (negate) return '-' + val;
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   readFloat() {
 | |
|     const val = this.buf.readFloatLE(this.pos);
 | |
|     this.pos += 4;
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   readDouble() {
 | |
|     const val = this.buf.readDoubleLE(this.pos);
 | |
|     this.pos += 8;
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   readIntLengthEncoded() {
 | |
|     const len = this.buf[this.pos++];
 | |
|     if (len === 0xfb) return null;
 | |
|     return this._atoi(len);
 | |
|   }
 | |
| 
 | |
|   _atoi(len) {
 | |
|     let result = 0;
 | |
|     let negate = false;
 | |
|     let begin = this.pos;
 | |
| 
 | |
|     if (len > 0 && this.buf[begin] === 45) {
 | |
|       //minus sign
 | |
|       negate = true;
 | |
|       begin++;
 | |
|     }
 | |
|     for (; begin < this.pos + len; begin++) {
 | |
|       result = result * 10 + (this.buf[begin] - 48);
 | |
|     }
 | |
|     this.pos += len;
 | |
|     return negate ? -1 * result : result;
 | |
|   }
 | |
| 
 | |
|   readFloatLengthCoded() {
 | |
|     const len = this.readUnsignedLength();
 | |
|     if (len === null) return null;
 | |
|     this.pos += len;
 | |
|     return +this.buf.toString('ascii', this.pos - len, this.pos);
 | |
|   }
 | |
| 
 | |
|   skipLengthCodedNumber() {
 | |
|     const type = this.buf[this.pos++];
 | |
|     switch (type) {
 | |
|       case 251:
 | |
|         return;
 | |
|       case 252:
 | |
|         this.pos += 2 + (0xffff & (this.buf[this.pos] + (this.buf[this.pos + 1] << 8)));
 | |
|         return;
 | |
|       case 253:
 | |
|         this.pos +=
 | |
|           3 + (0xffffff & (this.buf[this.pos] + (this.buf[this.pos + 1] << 8) + (this.buf[this.pos + 2] << 16)));
 | |
|         return;
 | |
|       case 254:
 | |
|         this.pos += 8 + Number(this.buf.readBigUInt64LE(this.pos));
 | |
|         return;
 | |
|       default:
 | |
|         this.pos += type;
 | |
|         return;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   length() {
 | |
|     return this.end - this.pos;
 | |
|   }
 | |
| 
 | |
|   subPacketLengthEncoded(len) {}
 | |
| 
 | |
|   /**
 | |
|    * Parse ERR_Packet : https://mariadb.com/kb/en/library/err_packet/
 | |
|    *
 | |
|    * @param info              current connection info
 | |
|    * @param sql               command sql
 | |
|    * @param stack             additional stack trace
 | |
|    * @returns {Error}
 | |
|    */
 | |
|   readError(info, sql, stack) {
 | |
|     this.skip(1);
 | |
|     let errno = this.readUInt16();
 | |
|     let sqlState;
 | |
|     let msg;
 | |
|     // check '#'
 | |
|     if (this.peek() === 0x23) {
 | |
|       // skip '#'
 | |
|       this.skip(6);
 | |
|       sqlState = this.buf.toString(undefined, this.pos - 5, this.pos);
 | |
|       msg = this.readStringNullEnded();
 | |
|     } else {
 | |
|       // pre 4.1 format
 | |
|       sqlState = 'HY000';
 | |
|       msg = this.buf.toString(undefined, this.pos, this.end);
 | |
|     }
 | |
|     let fatal = sqlState.startsWith('08') || sqlState === '70100';
 | |
|     return Errors.createError(msg, errno, info, sqlState, sql, fatal, stack);
 | |
|   }
 | |
| }
 | |
| 
 | |
| const appendZero = (val, len) => {
 | |
|   let st = val.toString();
 | |
|   while (st.length < len) {
 | |
|     st = '0' + st;
 | |
|   }
 | |
|   return st;
 | |
| };
 | |
| 
 | |
| module.exports = Packet;
 |