UNPKG

oracledb

Version:

A Node.js module for Oracle Database access from JavaScript and TypeScript

631 lines (564 loc) 18.5 kB
// Copyright (c) 2022, 2024, Oracle and/or its affiliates. //----------------------------------------------------------------------------- // // This software is dual-licensed to you under the Universal Permissive License // (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License // 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose // either license. // // If you elect to accept the software under the Apache License, Version 2.0, // the following applies: // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //----------------------------------------------------------------------------- 'use strict'; const { BaseBuffer } = require('../../impl/datahandlers/buffer.js'); const { Buffer } = require('buffer'); const constants = require('./constants.js'); const oson = require('../../impl/datahandlers/oson.js'); const utils = require('./utils.js'); const vector = require('../../impl/datahandlers/vector.js'); const errors = require("../../errors.js"); const TNS_BASE64_ALPHABET_ARRAY = Buffer.from("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", 'utf8'); const FAST_AUTH_END_OF_RPC_VALUE = 0x800; const FAST_AUTH_END_OF_RPC_OFFSET = 0x8; const MSG_TYPE_OFFSET = 11; /** * Class used for byte chunks used in the ChunkedBytesBuffer. */ class BytesChunk { /** * Constructor. * @param {Number} number of bytes to add to the chunk (rounded to the * nearest chunk size to avoid unnecessary allocations and copies) */ constructor(numBytes) { this.allocLen = numBytes; const remainder = numBytes % constants.CHUNKED_BYTES_CHUNK_SIZE; if (remainder > 0) { this.allocLen += (constants.CHUNKED_BYTES_CHUNK_SIZE - remainder); } this.buf = Buffer.allocUnsafe(this.allocLen); this.actualLen = 0; } } /** * Class used for handling chunked reads. */ class ChunkedBytesBuffer { /** * Constructor. */ constructor() { this.chunks = []; } /** * End the chunked read and return a consolidated buffer. */ endChunkedRead() { if (this.chunks.length > 1) { let totalNumBytes = 0; for (const chunk of this.chunks) { totalNumBytes += chunk.actualLen; } let pos = 0; const consolidatedChunk = new BytesChunk(totalNumBytes); for (const chunk of this.chunks) { chunk.buf.copy(consolidatedChunk.buf, pos, 0, chunk.actualLen); pos += chunk.actualLen; } consolidatedChunk.actualLen = totalNumBytes; this.chunks = [consolidatedChunk]; } const chunk = this.chunks[0]; return chunk.buf.subarray(0, chunk.actualLen); } /** * Constructor. */ getBuf(numBytes) { let chunk; if (this.chunks.length > 0) { chunk = this.chunks[this.chunks.length - 1]; if (chunk.allocLen - chunk.actualLen < numBytes) { chunk = undefined; } } if (!chunk) { chunk = new BytesChunk(numBytes); this.chunks.push(chunk); } const buf = chunk.buf.subarray(chunk.actualLen, chunk.actualLen + numBytes); chunk.actualLen += numBytes; return buf; } /** * Start a chunked read. This ensures that only one chunk is available and * its actual length is set to zero. */ startChunkedRead() { if (this.chunks.length > 0) { this.chunks = this.chunks.splice(0, 1); this.chunks[0].actualLen = 0; } } } /** * Encapsulates the Network Read Buffer * * @class ReadPacket */ class ReadPacket extends BaseBuffer { /** * Constructor. * @param {Object} adapter used for sending/receiving data * @param {Object} capabilities */ constructor(nsi, caps) { super(); this.nsi = nsi; this.caps = caps; this.chunkedBytesBuf = new ChunkedBytesBuffer(); } /** * Helper function that processes the length. If the length is defined as * TNS_LONG_LENGTH_INDICATOR, a chunked read is performed. */ _readBytesWithLength(numBytes) { if (numBytes !== constants.TNS_LONG_LENGTH_INDICATOR) { return this.readBytes(numBytes); } this.chunkedBytesBuf.startChunkedRead(); while (true) { // eslint-disable-line const numBytesInChunk = this.readUB4(); if (numBytesInChunk === 0) { break; } this.readBytes(numBytesInChunk, true); } return this.chunkedBytesBuf.endChunkedRead(); } skipBytes(numBytes) { // if no bytes are left in the buffer, a new packet needs to be fetched // before anything else can take place if (this.pos === this.size) { this.receivePacket(); } // if there is enough room in the buffer to satisfy the number of bytes // requested, return the buffer directly const numBytesLeft = this.numBytesLeft(); if (numBytes <= numBytesLeft) { this.pos += numBytes; return; } numBytes -= numBytesLeft; // acquire packets until the requested number of bytes is satisfied while (numBytes > 0) { this.receivePacket(); const numSplitBytes = Math.min(numBytes, this.size - this.pos); this.pos += numSplitBytes; numBytes -= numSplitBytes; } } /** * Returns a buffer containing the specified number of bytes. If an * insufficient number of bytes are available, a new packet is read. * @param {Number} specifies the number of bytes to read from the buffer */ readBytes(numBytes, inChunkedRead = false) { // if no bytes are left in the buffer, a new packet needs to be fetched // before anything else can take place if (this.pos === this.size) { this.receivePacket(); } // if there is enough room in the buffer to satisfy the number of bytes // requested, return the buffer directly const numBytesLeft = this.numBytesLeft(); if (numBytes <= numBytesLeft) { let buf; if (inChunkedRead) { buf = this.chunkedBytesBuf.getBuf(numBytes); this.buf.copy(buf, 0, this.pos, this.pos + numBytes); } else { buf = this.buf.subarray(this.pos, this.pos + numBytes); } this.pos += numBytes; return buf; } // the requested bytes are split across multiple packets; if a chunked read // is in progress, a chunk is acquired that will accommodate the requested // bytes; otherwise, a separate buffer will be allocated to accommodate the // requested bytes let buf; if (inChunkedRead) { buf = this.chunkedBytesBuf.getBuf(numBytes); } else { buf = Buffer.allocUnsafe(numBytes); } // copy the bytes to the buffer from the remainder of this packet let offset = 0; this.buf.copy(buf, offset, this.pos, this.pos + numBytesLeft); offset += numBytesLeft; numBytes -= numBytesLeft; // acquire packets until the requested number of bytes is satisfied while (numBytes > 0) { this.receivePacket(); const numSplitBytes = Math.min(numBytes, this.size - this.pos); this.buf.copy(buf, offset, this.pos, this.pos + numSplitBytes); this.pos += numSplitBytes; offset += numSplitBytes; numBytes -= numSplitBytes; } return buf; } /** * Receives a packet from the adapter. */ receivePacket() { if (this.savedPacketPos === this.savedPackets.length) { const packet = this.nsi.syncRecvPacket(); if (!packet || this.nsi.isBreak) throw new utils.OutOfPacketsError(); this.savedPackets.push(packet); } this.startPacket(this.savedPackets[this.savedPacketPos++]); } restorePoint() { this.savedPacketPos = 0; this.startPacket(this.savedPackets[this.savedPacketPos++]); this.pos = this.savedPos; } savePoint() { if (this.savedPackets) { this.savedPackets = this.savedPackets.splice(this.savedPacketPos - 1); } else { this.savedPackets = [this.packet]; } this.savedPacketPos = 1; this.savedPos = this.pos; } startPacket(packet) { this.packet = packet; this.buf = packet.buf; this.pos = 10; // skip packet heaader and data flags this.size = packet.buf.length; this.packetNum = packet.num; } /** * Read packets from network. * If checkRequestBoundary is passed as true, it * would read all packets until end of request * boundary is seen in nwk header. */ async waitForPackets(checkRequestBoundary = false) { let packet = await this.nsi.recvPacket(); if (!this.savedPackets) { this.savedPackets = [packet]; this.savedPacketPos = 0; } else { this.savedPackets.push(packet); } if (checkRequestBoundary && this.nsi.endOfRequestSupport) { while (packet.type === constants.TNS_PACKET_TYPE_DATA) { // End Marker if ((packet.buf.readUInt16BE(8) & constants.TNS_DATA_FLAGS_END_OF_REQUEST)) { break; } // Single Byte 1D packet if (packet.buf.length === MSG_TYPE_OFFSET && packet.buf[MSG_TYPE_OFFSET - 1] === constants.TNS_MSG_TYPE_END_OF_REQUEST) { break; } packet = await this.nsi.recvPacket(); this.savedPackets.push(packet); } } this.startPacket(this.savedPackets[this.savedPacketPos++]); } /** * Reads OSON (QLocator followed by data) and decodes it into a JavaScript * object. */ readOson() { const numBytes = this.readUB4(); if (numBytes === 0) { return null; } this.skipUB8(); // size (unused) this.skipUB4(); // chunk size (unused) const decoder = new oson.OsonDecoder(this.readBytesWithLength()); this.skipBytesChunked(); // locator (unused) return decoder.decode(); } readURowID() { let outputOffset = 0, inputOffset = 1; let buf = this.readBytesWithLength(); if (buf === null) return null; buf = this.readBytesWithLength(); let inputLen = buf.length; // Handle physical rowid if (buf && buf[0] === 1) { const rba = buf.readUInt32BE(1); const partitionID = buf.readUInt16BE(5); const blockNum = buf.readUInt32BE(7); const slotNum = buf.readUInt16BE(11); return utils.encodeRowID({rba, partitionID, blockNum, slotNum}); } // handle logical rowid let outputLen = Math.floor(inputLen / 3) * 4; const remainder = inputLen % 3; if (remainder === 1) { outputLen += 1; } else if (remainder === 2) { outputLen += 3; } const outputValue = Buffer.allocUnsafe(outputLen); inputLen -= 1; outputValue[0] = 42; outputOffset += 1; while (inputLen > 0) { // produce first byte of quadruple let pos = buf[inputOffset] >> 2; outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos]; outputOffset += 1; // produce second byte of quadruple, but if only one byte is left, // produce that one byte and exit pos = (buf[inputOffset] & 0x3) << 4; if (inputLen == 1) { outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos]; break; } inputOffset += 1; pos |= ((buf[inputOffset] & 0xf0) >> 4); outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos]; outputOffset += 1; // produce third byte of quadruple, but if only two bytes are left, // produce that one byte and exit pos = (buf[inputOffset] & 0xf) << 2; if (inputLen == 2) { outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos]; break; } inputOffset += 1; pos |= ((buf[inputOffset] & 0xc0) >> 6); outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos]; outputOffset += 1; // produce final byte of quadruple pos = buf[inputOffset] & 0x3f; outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos]; outputOffset += 1; inputOffset += 1; inputLen -= 3; } return outputValue.toString('utf-8'); } readRowID() { const rba = this.readUB4(); const partitionID = this.readUB2(); this.skipUB1(); const blockNum = this.readUB4(); const slotNum = this.readUB2(); return {rba, partitionID, blockNum, slotNum}; } /** * Reads VECTOR data (QLocator followed by data) and decodes it into a * JavaScript object. */ readVector() { const numBytes = this.readUB4(); if (numBytes === 0) { return null; } this.skipUB8(); // size (unused) this.skipUB4(); // chunk size (unused) const decoder = new vector.VectorDecoder(this.readBytesWithLength()); this.skipBytesChunked(); // locator (unused) return decoder.decode(); } skipBytesChunked() { const numBytes = this.readUInt8(); if (numBytes === 0 || numBytes === constants.TNS_NULL_LENGTH_INDICATOR) { return; } if (numBytes !== constants.TNS_LONG_LENGTH_INDICATOR) { this.skipBytes(numBytes); } else { while (true) { // eslint-disable-line const tempNumBytes = this.readUB4(); if (tempNumBytes === 0) break; this.skipBytes(tempNumBytes); } } } readNullTerminatedBytes(maxSize = 50) { let offset = 0; const tmp = Buffer.allocUnsafe(maxSize); while (offset < maxSize) { tmp[offset] = this.readUInt8(); if (tmp[offset] === 0) { break; } offset = offset + 1; } if (offset === maxSize) { const reason = `Byte Arrray Exceeded MaxSize ${maxSize}`; errors.throwErr(errors.ERR_INTERNAL, reason); } return tmp.subarray(0, offset + 1); } } /** * Encapsulates the Network Write Buffer * * @class WritePacket */ class WritePacket extends BaseBuffer { constructor(nsi, caps, protocol) { super(nsi.sAtts.sdu); this.size = this.maxSize; this.isLargeSDU = nsi.sAtts.version >= constants.TNS_VERSION_MIN_LARGE_SDU; this.protocol = protocol; this.packetType = constants.TNS_PACKET_TYPE_DATA; this.caps = caps; this.nsi = nsi; } /** * Grows the buffer by sending the existing buffer on the transport. A copy * is made so that the existing buffer can be used for the next batch of data * that needs to be sent */ _grow() { this._sendPacket(); } /** * Sends the data in the buffer on the transport. First, the packet header is * set up by writing the size and packet type. */ _sendPacket(finalPacket = false) { const size = this.pos; this.pos = 0; if (this.isLargeSDU) { this.writeUInt32BE(size); } else { this.writeUInt16BE(size); this.writeUInt16BE(0); } this.writeUInt8(this.packetType); this.writeUInt8(0); this.writeUInt16BE(0); let buf = this.buf.subarray(0, size); if (!finalPacket) { buf = Buffer.from(buf); this.startPacket(); } else { // Write End of RPC bit in last packet used only for fastAuth Message. this.buf.writeUInt16BE(FAST_AUTH_END_OF_RPC_VALUE, FAST_AUTH_END_OF_RPC_OFFSET); } if (!this.nsi.ntAdapter) { errors.throwErr(errors.ERR_INVALID_CONNECTION); } this.nsi.sendPacket(buf); } /** * Starts a packet. */ startPacket(dataFlags = 0) { this.pos = constants.PACKET_HEADER_SIZE; if (this.packetType === constants.TNS_PACKET_TYPE_DATA) { this.writeUInt16BE(dataFlags); } } /** * Starts a database request. */ startRequest(packetType, dataFlags = 0) { this.packetType = packetType; this.startPacket(dataFlags); } /** * Ends a database request. */ endRequest() { if (this.pos > constants.PACKET_HEADER_SIZE) { this._sendPacket(true); } } writeKeyValue(key, value, flags = 0) { const keyBytesLen = Buffer.byteLength(key); const valBytesLen = Buffer.byteLength(value); this.writeUB4(keyBytesLen); this.writeBytesWithLength(Buffer.from(key)); this.writeUB4(valBytesLen); if (valBytesLen > 0) { this.writeBytesWithLength(Buffer.from(value)); } this.writeUB4(flags); } /** * Encodes a JavaScript object into OSON and then writes it (QLocator * followed by data) to the buffer. */ writeOson(value, osonMaxFieldSize) { const encoder = new oson.OsonEncoder(); const buf = encoder.encode(value, osonMaxFieldSize); this.writeQLocator(buf.length); this.writeBytesWithLength(buf); } writeSeqNum() { this.writeUInt8(this.protocol.sequenceId); this.protocol.sequenceId = (this.protocol.sequenceId + 1) % 256; } /** * Encodes a JavaScript object into VECTOR data and then writes it (QLocator * followed by data) to the buffer. */ writeVector(value) { const encoder = new vector.VectorEncoder(); const buf = encoder.encode(value); this.writeQLocator(buf.length); this.writeBytesWithLength(buf); } //--------------------------------------------------------------------------- // writeQLocator() // // Writes a QLocator. QLocators are always 40 bytes in length. //--------------------------------------------------------------------------- writeQLocator(numBytes) { this.writeUB4(40); // QLocator length this.writeUInt8(40); // repeated length this.writeUInt16BE(38); // internal length this.writeUInt16BE(constants.TNS_LOB_QLOCATOR_VERSION); this.writeUInt8(constants.TNS_LOB_LOC_FLAGS_VALUE_BASED | constants.TNS_LOB_LOC_FLAGS_BLOB | constants.TNS_LOB_LOC_FLAGS_ABSTRACT); this.writeUInt8(constants.TNS_LOB_LOC_FLAGS_INIT); this.writeUInt16BE(0); // additional flags this.writeUInt16BE(1); // byt1 this.writeUInt64BE(numBytes); this.writeUInt16BE(0); // unused this.writeUInt16BE(0); // csid this.writeUInt16BE(0); // unused this.writeUInt64BE(0); // unused this.writeUInt64BE(0); // unused } } module.exports = { ReadPacket, WritePacket };