mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
624 lines
20 KiB
JavaScript
624 lines
20 KiB
JavaScript
/* 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/. */
|
|
|
|
|
|
/**
|
|
* This object contains helpers buffering incoming data & deconstructing it
|
|
* into parcels as well as buffering outgoing data & constructing parcels.
|
|
* For that it maintains two buffers and corresponding uint8 views, indexes.
|
|
*
|
|
* The incoming buffer is a circular buffer where we store incoming data.
|
|
* As soon as a complete parcel is received, it is processed right away, so
|
|
* the buffer only needs to be large enough to hold one parcel.
|
|
*
|
|
* The outgoing buffer is to prepare outgoing parcels. The index is reset
|
|
* every time a parcel is sent.
|
|
*/
|
|
|
|
let Buf = {
|
|
INT32_MAX: 2147483647,
|
|
UINT8_SIZE: 1,
|
|
UINT16_SIZE: 2,
|
|
UINT32_SIZE: 4,
|
|
PARCEL_SIZE_SIZE: 4,
|
|
PDU_HEX_OCTET_SIZE: 4,
|
|
|
|
incomingBufferLength: 1024,
|
|
incomingBuffer: null,
|
|
incomingBytes: null,
|
|
incomingWriteIndex: 0,
|
|
incomingReadIndex: 0,
|
|
readIncoming: 0,
|
|
readAvailable: 0,
|
|
currentParcelSize: 0,
|
|
|
|
outgoingBufferLength: 1024,
|
|
outgoingBuffer: null,
|
|
outgoingBytes: null,
|
|
outgoingIndex: 0,
|
|
outgoingBufferCalSizeQueue: null,
|
|
|
|
_init: function() {
|
|
this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength);
|
|
this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength);
|
|
|
|
this.incomingBytes = new Uint8Array(this.incomingBuffer);
|
|
this.outgoingBytes = new Uint8Array(this.outgoingBuffer);
|
|
|
|
// Track where incoming data is read from and written to.
|
|
this.incomingWriteIndex = 0;
|
|
this.incomingReadIndex = 0;
|
|
|
|
// Leave room for the parcel size for outgoing parcels.
|
|
this.outgoingIndex = this.PARCEL_SIZE_SIZE;
|
|
|
|
// How many bytes we've read for this parcel so far.
|
|
this.readIncoming = 0;
|
|
|
|
// How many bytes available as parcel data.
|
|
this.readAvailable = 0;
|
|
|
|
// Size of the incoming parcel. If this is zero, we're expecting a new
|
|
// parcel.
|
|
this.currentParcelSize = 0;
|
|
|
|
// Queue for storing outgoing override points
|
|
this.outgoingBufferCalSizeQueue = [];
|
|
},
|
|
|
|
/**
|
|
* Mark current outgoingIndex as start point for calculation length of data
|
|
* written to outgoingBuffer.
|
|
* Mark can be nested for here uses queue to remember marks.
|
|
*
|
|
* @param writeFunction
|
|
* Function to write data length into outgoingBuffer, this function is
|
|
* also used to allocate buffer for data length.
|
|
* Raw data size(in Uint8) is provided as parameter calling writeFunction.
|
|
* If raw data size is not in proper unit for writing, user can adjust
|
|
* the length value in writeFunction before writing.
|
|
**/
|
|
startCalOutgoingSize: function(writeFunction) {
|
|
let sizeInfo = {index: this.outgoingIndex,
|
|
write: writeFunction};
|
|
|
|
// Allocate buffer for data lemgtj.
|
|
writeFunction.call(0);
|
|
|
|
// Get size of data length buffer for it is not counted into data size.
|
|
sizeInfo.size = this.outgoingIndex - sizeInfo.index;
|
|
|
|
// Enqueue size calculation information.
|
|
this.outgoingBufferCalSizeQueue.push(sizeInfo);
|
|
},
|
|
|
|
/**
|
|
* Calculate data length since last mark, and write it into mark position.
|
|
**/
|
|
stopCalOutgoingSize: function() {
|
|
let sizeInfo = this.outgoingBufferCalSizeQueue.pop();
|
|
|
|
// Remember current outgoingIndex.
|
|
let currentOutgoingIndex = this.outgoingIndex;
|
|
// Calculate data length, in uint8.
|
|
let writeSize = this.outgoingIndex - sizeInfo.index - sizeInfo.size;
|
|
|
|
// Write data length to mark, use same function for allocating buffer to make
|
|
// sure there is no buffer overloading.
|
|
this.outgoingIndex = sizeInfo.index;
|
|
sizeInfo.write(writeSize);
|
|
|
|
// Restore outgoingIndex.
|
|
this.outgoingIndex = currentOutgoingIndex;
|
|
},
|
|
|
|
/**
|
|
* Grow the incoming buffer.
|
|
*
|
|
* @param min_size
|
|
* Minimum new size. The actual new size will be the the smallest
|
|
* power of 2 that's larger than this number.
|
|
*/
|
|
growIncomingBuffer: function(min_size) {
|
|
if (DEBUG) {
|
|
debug("Current buffer of " + this.incomingBufferLength +
|
|
" can't handle incoming " + min_size + " bytes.");
|
|
}
|
|
let oldBytes = this.incomingBytes;
|
|
this.incomingBufferLength =
|
|
2 << Math.floor(Math.log(min_size)/Math.log(2));
|
|
if (DEBUG) debug("New incoming buffer size: " + this.incomingBufferLength);
|
|
this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength);
|
|
this.incomingBytes = new Uint8Array(this.incomingBuffer);
|
|
if (this.incomingReadIndex <= this.incomingWriteIndex) {
|
|
// Read and write index are in natural order, so we can just copy
|
|
// the old buffer over to the bigger one without having to worry
|
|
// about the indexes.
|
|
this.incomingBytes.set(oldBytes, 0);
|
|
} else {
|
|
// The write index has wrapped around but the read index hasn't yet.
|
|
// Write whatever the read index has left to read until it would
|
|
// circle around to the beginning of the new buffer, and the rest
|
|
// behind that.
|
|
let head = oldBytes.subarray(this.incomingReadIndex);
|
|
let tail = oldBytes.subarray(0, this.incomingReadIndex);
|
|
this.incomingBytes.set(head, 0);
|
|
this.incomingBytes.set(tail, head.length);
|
|
this.incomingReadIndex = 0;
|
|
this.incomingWriteIndex += head.length;
|
|
}
|
|
if (DEBUG) {
|
|
debug("New incoming buffer size is " + this.incomingBufferLength);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Grow the outgoing buffer.
|
|
*
|
|
* @param min_size
|
|
* Minimum new size. The actual new size will be the the smallest
|
|
* power of 2 that's larger than this number.
|
|
*/
|
|
growOutgoingBuffer: function(min_size) {
|
|
if (DEBUG) {
|
|
debug("Current buffer of " + this.outgoingBufferLength +
|
|
" is too small.");
|
|
}
|
|
let oldBytes = this.outgoingBytes;
|
|
this.outgoingBufferLength =
|
|
2 << Math.floor(Math.log(min_size)/Math.log(2));
|
|
this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength);
|
|
this.outgoingBytes = new Uint8Array(this.outgoingBuffer);
|
|
this.outgoingBytes.set(oldBytes, 0);
|
|
if (DEBUG) {
|
|
debug("New outgoing buffer size is " + this.outgoingBufferLength);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Functions for reading data from the incoming buffer.
|
|
*
|
|
* These are all little endian, apart from readParcelSize();
|
|
*/
|
|
|
|
/**
|
|
* Ensure position specified is readable.
|
|
*
|
|
* @param index
|
|
* Data position in incoming parcel, valid from 0 to
|
|
* currentParcelSize.
|
|
*/
|
|
ensureIncomingAvailable: function(index) {
|
|
if (index >= this.currentParcelSize) {
|
|
throw new Error("Trying to read data beyond the parcel end!");
|
|
} else if (index < 0) {
|
|
throw new Error("Trying to read data before the parcel begin!");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Seek in current incoming parcel.
|
|
*
|
|
* @param offset
|
|
* Seek offset in relative to current position.
|
|
*/
|
|
seekIncoming: function(offset) {
|
|
// Translate to 0..currentParcelSize
|
|
let cur = this.currentParcelSize - this.readAvailable;
|
|
|
|
let newIndex = cur + offset;
|
|
this.ensureIncomingAvailable(newIndex);
|
|
|
|
// ... incomingReadIndex -->|
|
|
// 0 new cur currentParcelSize
|
|
// |================|=======|====================|
|
|
// |<-- cur -->|<- readAvailable ->|
|
|
// |<-- newIndex -->|<-- new readAvailable -->|
|
|
this.readAvailable = this.currentParcelSize - newIndex;
|
|
|
|
// Translate back:
|
|
if (this.incomingReadIndex < cur) {
|
|
// The incomingReadIndex is wrapped.
|
|
newIndex += this.incomingBufferLength;
|
|
}
|
|
newIndex += (this.incomingReadIndex - cur);
|
|
newIndex %= this.incomingBufferLength;
|
|
this.incomingReadIndex = newIndex;
|
|
},
|
|
|
|
readUint8Unchecked: function() {
|
|
let value = this.incomingBytes[this.incomingReadIndex];
|
|
this.incomingReadIndex = (this.incomingReadIndex + 1) %
|
|
this.incomingBufferLength;
|
|
return value;
|
|
},
|
|
|
|
readUint8: function() {
|
|
// Translate to 0..currentParcelSize
|
|
let cur = this.currentParcelSize - this.readAvailable;
|
|
this.ensureIncomingAvailable(cur);
|
|
|
|
this.readAvailable--;
|
|
return this.readUint8Unchecked();
|
|
},
|
|
|
|
readUint8Array: function(length) {
|
|
// Translate to 0..currentParcelSize
|
|
let last = this.currentParcelSize - this.readAvailable;
|
|
last += (length - 1);
|
|
this.ensureIncomingAvailable(last);
|
|
|
|
let array = new Uint8Array(length);
|
|
for (let i = 0; i < length; i++) {
|
|
array[i] = this.readUint8Unchecked();
|
|
}
|
|
|
|
this.readAvailable -= length;
|
|
return array;
|
|
},
|
|
|
|
readUint16: function() {
|
|
return this.readUint8() | this.readUint8() << 8;
|
|
},
|
|
|
|
readInt32: function() {
|
|
return this.readUint8() | this.readUint8() << 8 |
|
|
this.readUint8() << 16 | this.readUint8() << 24;
|
|
},
|
|
|
|
readInt64: function() {
|
|
// Avoid using bitwise operators as the operands of all bitwise operators
|
|
// are converted to signed 32-bit integers.
|
|
return this.readUint8() +
|
|
this.readUint8() * Math.pow(2, 8) +
|
|
this.readUint8() * Math.pow(2, 16) +
|
|
this.readUint8() * Math.pow(2, 24) +
|
|
this.readUint8() * Math.pow(2, 32) +
|
|
this.readUint8() * Math.pow(2, 40) +
|
|
this.readUint8() * Math.pow(2, 48) +
|
|
this.readUint8() * Math.pow(2, 56);
|
|
},
|
|
|
|
readInt32List: function() {
|
|
let length = this.readInt32();
|
|
let ints = [];
|
|
for (let i = 0; i < length; i++) {
|
|
ints.push(this.readInt32());
|
|
}
|
|
return ints;
|
|
},
|
|
|
|
readString: function() {
|
|
let string_len = this.readInt32();
|
|
if (string_len < 0 || string_len >= this.INT32_MAX) {
|
|
return null;
|
|
}
|
|
let s = "";
|
|
for (let i = 0; i < string_len; i++) {
|
|
s += String.fromCharCode(this.readUint16());
|
|
}
|
|
// Strings are \0\0 delimited, but that isn't part of the length. And
|
|
// if the string length is even, the delimiter is two characters wide.
|
|
// It's insane, I know.
|
|
this.readStringDelimiter(string_len);
|
|
return s;
|
|
},
|
|
|
|
readStringList: function() {
|
|
let num_strings = this.readInt32();
|
|
let strings = [];
|
|
for (let i = 0; i < num_strings; i++) {
|
|
strings.push(this.readString());
|
|
}
|
|
return strings;
|
|
},
|
|
|
|
readStringDelimiter: function(length) {
|
|
let delimiter = this.readUint16();
|
|
if (!(length & 1)) {
|
|
delimiter |= this.readUint16();
|
|
}
|
|
if (DEBUG) {
|
|
if (delimiter !== 0) {
|
|
debug("Something's wrong, found string delimiter: " + delimiter);
|
|
}
|
|
}
|
|
},
|
|
|
|
readParcelSize: function() {
|
|
return this.readUint8Unchecked() << 24 |
|
|
this.readUint8Unchecked() << 16 |
|
|
this.readUint8Unchecked() << 8 |
|
|
this.readUint8Unchecked();
|
|
},
|
|
|
|
/**
|
|
* Functions for writing data to the outgoing buffer.
|
|
*/
|
|
|
|
/**
|
|
* Ensure position specified is writable.
|
|
*
|
|
* @param index
|
|
* Data position in outgoing parcel, valid from 0 to
|
|
* outgoingBufferLength.
|
|
*/
|
|
ensureOutgoingAvailable: function(index) {
|
|
if (index >= this.outgoingBufferLength) {
|
|
this.growOutgoingBuffer(index + 1);
|
|
}
|
|
},
|
|
|
|
writeUint8: function(value) {
|
|
this.ensureOutgoingAvailable(this.outgoingIndex);
|
|
|
|
this.outgoingBytes[this.outgoingIndex] = value;
|
|
this.outgoingIndex++;
|
|
},
|
|
|
|
writeUint16: function(value) {
|
|
this.writeUint8(value & 0xff);
|
|
this.writeUint8((value >> 8) & 0xff);
|
|
},
|
|
|
|
writeInt32: function(value) {
|
|
this.writeUint8(value & 0xff);
|
|
this.writeUint8((value >> 8) & 0xff);
|
|
this.writeUint8((value >> 16) & 0xff);
|
|
this.writeUint8((value >> 24) & 0xff);
|
|
},
|
|
|
|
writeString: function(value) {
|
|
if (value == null) {
|
|
this.writeInt32(-1);
|
|
return;
|
|
}
|
|
this.writeInt32(value.length);
|
|
for (let i = 0; i < value.length; i++) {
|
|
this.writeUint16(value.charCodeAt(i));
|
|
}
|
|
// Strings are \0\0 delimited, but that isn't part of the length. And
|
|
// if the string length is even, the delimiter is two characters wide.
|
|
// It's insane, I know.
|
|
this.writeStringDelimiter(value.length);
|
|
},
|
|
|
|
writeStringList: function(strings) {
|
|
this.writeInt32(strings.length);
|
|
for (let i = 0; i < strings.length; i++) {
|
|
this.writeString(strings[i]);
|
|
}
|
|
},
|
|
|
|
writeStringDelimiter: function(length) {
|
|
this.writeUint16(0);
|
|
if (!(length & 1)) {
|
|
this.writeUint16(0);
|
|
}
|
|
},
|
|
|
|
writeParcelSize: function(value) {
|
|
/**
|
|
* Parcel size will always be the first thing in the parcel byte
|
|
* array, but the last thing written. Store the current index off
|
|
* to a temporary to be reset after we write the size.
|
|
*/
|
|
let currentIndex = this.outgoingIndex;
|
|
this.outgoingIndex = 0;
|
|
this.writeUint8((value >> 24) & 0xff);
|
|
this.writeUint8((value >> 16) & 0xff);
|
|
this.writeUint8((value >> 8) & 0xff);
|
|
this.writeUint8(value & 0xff);
|
|
this.outgoingIndex = currentIndex;
|
|
},
|
|
|
|
copyIncomingToOutgoing: function(length) {
|
|
if (!length || (length < 0)) {
|
|
return;
|
|
}
|
|
|
|
let translatedReadIndexEnd =
|
|
this.currentParcelSize - this.readAvailable + length - 1;
|
|
this.ensureIncomingAvailable(translatedReadIndexEnd);
|
|
|
|
let translatedWriteIndexEnd = this.outgoingIndex + length - 1;
|
|
this.ensureOutgoingAvailable(translatedWriteIndexEnd);
|
|
|
|
let newIncomingReadIndex = this.incomingReadIndex + length;
|
|
if (newIncomingReadIndex < this.incomingBufferLength) {
|
|
// Reading won't cause wrapping, go ahead with builtin copy.
|
|
this.outgoingBytes
|
|
.set(this.incomingBytes.subarray(this.incomingReadIndex,
|
|
newIncomingReadIndex),
|
|
this.outgoingIndex);
|
|
} else {
|
|
// Not so lucky.
|
|
newIncomingReadIndex %= this.incomingBufferLength;
|
|
this.outgoingBytes
|
|
.set(this.incomingBytes.subarray(this.incomingReadIndex,
|
|
this.incomingBufferLength),
|
|
this.outgoingIndex);
|
|
if (newIncomingReadIndex) {
|
|
let firstPartLength = this.incomingBufferLength - this.incomingReadIndex;
|
|
this.outgoingBytes.set(this.incomingBytes.subarray(0, newIncomingReadIndex),
|
|
this.outgoingIndex + firstPartLength);
|
|
}
|
|
}
|
|
|
|
this.incomingReadIndex = newIncomingReadIndex;
|
|
this.readAvailable -= length;
|
|
this.outgoingIndex += length;
|
|
},
|
|
|
|
/**
|
|
* Parcel management
|
|
*/
|
|
|
|
/**
|
|
* Write incoming data to the circular buffer.
|
|
*
|
|
* @param incoming
|
|
* Uint8Array containing the incoming data.
|
|
*/
|
|
writeToIncoming: function(incoming) {
|
|
// We don't have to worry about the head catching the tail since
|
|
// we process any backlog in parcels immediately, before writing
|
|
// new data to the buffer. So the only edge case we need to handle
|
|
// is when the incoming data is larger than the buffer size.
|
|
let minMustAvailableSize = incoming.length + this.readIncoming;
|
|
if (minMustAvailableSize > this.incomingBufferLength) {
|
|
this.growIncomingBuffer(minMustAvailableSize);
|
|
}
|
|
|
|
// We can let the typed arrays do the copying if the incoming data won't
|
|
// wrap around the edges of the circular buffer.
|
|
let remaining = this.incomingBufferLength - this.incomingWriteIndex;
|
|
if (remaining >= incoming.length) {
|
|
this.incomingBytes.set(incoming, this.incomingWriteIndex);
|
|
} else {
|
|
// The incoming data would wrap around it.
|
|
let head = incoming.subarray(0, remaining);
|
|
let tail = incoming.subarray(remaining);
|
|
this.incomingBytes.set(head, this.incomingWriteIndex);
|
|
this.incomingBytes.set(tail, 0);
|
|
}
|
|
this.incomingWriteIndex = (this.incomingWriteIndex + incoming.length) %
|
|
this.incomingBufferLength;
|
|
},
|
|
|
|
/**
|
|
* Process incoming data.
|
|
*
|
|
* @param incoming
|
|
* Uint8Array containing the incoming data.
|
|
*/
|
|
processIncoming: function(incoming) {
|
|
if (DEBUG) {
|
|
debug("Received " + incoming.length + " bytes.");
|
|
debug("Already read " + this.readIncoming);
|
|
}
|
|
|
|
this.writeToIncoming(incoming);
|
|
this.readIncoming += incoming.length;
|
|
while (true) {
|
|
if (!this.currentParcelSize) {
|
|
// We're expecting a new parcel.
|
|
if (this.readIncoming < this.PARCEL_SIZE_SIZE) {
|
|
// We don't know how big the next parcel is going to be, need more
|
|
// data.
|
|
if (DEBUG) debug("Next parcel size unknown, going to sleep.");
|
|
return;
|
|
}
|
|
this.currentParcelSize = this.readParcelSize();
|
|
if (DEBUG) {
|
|
debug("New incoming parcel of size " + this.currentParcelSize);
|
|
}
|
|
// The size itself is not included in the size.
|
|
this.readIncoming -= this.PARCEL_SIZE_SIZE;
|
|
}
|
|
|
|
if (this.readIncoming < this.currentParcelSize) {
|
|
// We haven't read enough yet in order to be able to process a parcel.
|
|
if (DEBUG) debug("Read " + this.readIncoming + ", but parcel size is "
|
|
+ this.currentParcelSize + ". Going to sleep.");
|
|
return;
|
|
}
|
|
|
|
// Alright, we have enough data to process at least one whole parcel.
|
|
// Let's do that.
|
|
let expectedAfterIndex = (this.incomingReadIndex + this.currentParcelSize)
|
|
% this.incomingBufferLength;
|
|
|
|
if (DEBUG) {
|
|
let parcel;
|
|
if (expectedAfterIndex < this.incomingReadIndex) {
|
|
let head = this.incomingBytes.subarray(this.incomingReadIndex);
|
|
let tail = this.incomingBytes.subarray(0, expectedAfterIndex);
|
|
parcel = Array.slice(head).concat(Array.slice(tail));
|
|
} else {
|
|
parcel = Array.slice(this.incomingBytes.subarray(
|
|
this.incomingReadIndex, expectedAfterIndex));
|
|
}
|
|
debug("Parcel (size " + this.currentParcelSize + "): " + parcel);
|
|
}
|
|
|
|
if (DEBUG) debug("We have at least one complete parcel.");
|
|
try {
|
|
this.readAvailable = this.currentParcelSize;
|
|
this.processParcel();
|
|
} catch (ex) {
|
|
if (DEBUG) debug("Parcel handling threw " + ex + "\n" + ex.stack);
|
|
}
|
|
|
|
// Ensure that the whole parcel was consumed.
|
|
if (this.incomingReadIndex != expectedAfterIndex) {
|
|
if (DEBUG) {
|
|
debug("Parcel handler didn't consume whole parcel, " +
|
|
Math.abs(expectedAfterIndex - this.incomingReadIndex) +
|
|
" bytes left over");
|
|
}
|
|
this.incomingReadIndex = expectedAfterIndex;
|
|
}
|
|
this.readIncoming -= this.currentParcelSize;
|
|
this.readAvailable = 0;
|
|
this.currentParcelSize = 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Communicate with the IPC thread.
|
|
*/
|
|
sendParcel: function() {
|
|
// Compute the size of the parcel and write it to the front of the parcel
|
|
// where we left room for it. Note that he parcel size does not include
|
|
// the size itself.
|
|
let parcelSize = this.outgoingIndex - this.PARCEL_SIZE_SIZE;
|
|
this.writeParcelSize(parcelSize);
|
|
|
|
// This assumes that postRILMessage will make a copy of the ArrayBufferView
|
|
// right away!
|
|
let parcel = this.outgoingBytes.subarray(0, this.outgoingIndex);
|
|
if (DEBUG) debug("Outgoing parcel: " + Array.slice(parcel));
|
|
this.onSendParcel(parcel);
|
|
this.outgoingIndex = this.PARCEL_SIZE_SIZE;
|
|
},
|
|
|
|
getCurrentParcelSize: function() {
|
|
return this.currentParcelSize;
|
|
},
|
|
|
|
getReadAvailable: function() {
|
|
return this.readAvailable;
|
|
}
|
|
|
|
/**
|
|
* Process one parcel.
|
|
*
|
|
* |processParcel| is an implementation provided incoming parcel processing
|
|
* function invoked when we have received a complete parcel. Implementation
|
|
* may call multiple read functions to extract data from the incoming buffer.
|
|
*/
|
|
//processParcel: function() {
|
|
// let something = this.readInt32();
|
|
// ...
|
|
//},
|
|
|
|
/**
|
|
* Write raw data out to underlying channel.
|
|
*
|
|
* |onSendParcel| is an implementation provided stream output function
|
|
* invoked when we're really going to write something out. We assume the
|
|
* data are completely copied to some output buffer in this call and may
|
|
* be destroyed when it's done.
|
|
*
|
|
* @param parcel
|
|
* An array of numeric octet data.
|
|
*/
|
|
//onSendParcel: function(parcel) {
|
|
// ...
|
|
//}
|
|
};
|
|
|
|
module.exports = { Buf: Buf };
|