blob: 3722dce6a660501f5b6c52ffac57d462e9a6d6d2 [file]
//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
#pragma once
namespace Js
{
template <typename TAllocator>
class StringBuilder
{
private:
struct Data
{
public:
union {
struct st_Single
{
char16 buffer[];
} single;
struct st_Chained
{
charcount_t length;
Data *next;
char16 buffer[];
} chained;
}u;
};
private:
static const charcount_t MaxLength = INT_MAX - 1;
const static charcount_t MaxRealloc = 64;
TAllocator* alloc;
// First chunk is just a buffer, and which can be detached without copying.
Data *firstChunk;
// Second chunk is a chained list of chunks. UnChain() needs to be called to copy the first chunk
// and the list of chained chunks to a single buffer on calls to GetBuffer().
Data *secondChunk;
Data *lastChunk;
char16 * appendPtr;
charcount_t length; // Total capacity (allocated number of elements - 1), in all chunks. Note that we keep one allocated element which is not accounted in length for terminating '\0'.
charcount_t count; // Total number of elements, in all chunks.
charcount_t firstChunkLength;
charcount_t initialSize;
bool IsChained() { return this->secondChunk != NULL; }
Data *NewChainedChunk(charcount_t bufLengthRequested)
{
CompileAssert(sizeof(charcount_t) == sizeof(uint32));
// allocation = (bufLengthRequested * sizeof(char16) + sizeof(Data)
charcount_t alloc32 = UInt32Math::MulAdd<sizeof(char16), sizeof(Data)>(bufLengthRequested);
size_t allocation = TAllocator::GetAlignedSize(alloc32);
size_t size_t_length = (allocation - sizeof(Data)) / sizeof(char16);
charcount_t bufLength = (charcount_t)size_t_length;
Assert(bufLength == size_t_length);
Data *newChunk = AllocatorNewStructPlus(TAllocator, this->alloc, allocation, Data);
newChunk->u.chained.length = bufLength;
newChunk->u.chained.next = NULL;
// Recycler gives zeroed memory, so rely on that instead of memsetting the tail
#if 0
// Align memset to machine register size for perf
bufLengthRequested &= ~(sizeof(size_t) - 1);
memset(newChunk->u.chained.buffer + bufLengthRequested, 0, (bufLength - bufLengthRequested) * sizeof(char16));
#endif
return newChunk;
}
Data *NewSingleChunk(charcount_t *pBufLengthRequested)
{
Assert(*pBufLengthRequested <= MaxLength);
// Let's just grow the current chunk in place
CompileAssert(sizeof(charcount_t) == sizeof(uint32));
//// allocation = (bufLengthRequested+1) * sizeof(char16)
charcount_t alloc32 = UInt32Math::AddMul< 1, sizeof(char16) >(*pBufLengthRequested);
size_t allocation = HeapInfo::GetAlignedSize(alloc32);
size_t size_t_newLength = allocation / sizeof(char16) - 1;
charcount_t newLength = (charcount_t)size_t_newLength;
Assert(newLength == size_t_newLength);
Assert(newLength <= MaxLength + 1);
if (newLength == MaxLength + 1)
{
// newLength could be MaxLength + 1 because of alignment.
// In this case alloc size is 2 elements more than newLength (normally 1 elements more for NULL), that's fine.
newLength = MaxLength;
}
Assert(newLength <= MaxLength);
Data* newChunk = AllocatorNewStructPlus(TAllocator, this->alloc, allocation, Data);
newChunk->u.single.buffer[newLength] = _u('\0');
*pBufLengthRequested = newLength;
return newChunk;
}
_NOINLINE void ExtendBuffer(charcount_t newLength)
{
Data *newChunk;
// To maintain this->length under MaxLength, check it here/throw, this is the only place we grow the buffer.
if (newLength > MaxLength)
{
Throw::OutOfMemory();
}
Assert(this->length <= MaxLength);
charcount_t newLengthTryGrowPolicy = newLength + (this->length*2/3); // Note: this would never result in uint32 overflow.
if (newLengthTryGrowPolicy <= MaxLength)
{
newLength = newLengthTryGrowPolicy;
}
Assert(newLength <= MaxLength);
// We already have linked chunks
if (this->IsChained() || (this->firstChunk != NULL && newLength - this->length > MaxRealloc))
{
newChunk = this->NewChainedChunk(newLength - this->count);
if (this->IsChained())
{
this->lastChunk->u.chained.next = newChunk;
// We're not going to use the extra space in the current chunk...
Assert(this->lastChunk->u.chained.length > this->length - this->count);
this->lastChunk->u.chained.length -= (this->length - this->count);
}
else
{
// Time to add our first linked chunk
Assert(this->secondChunk == NULL);
this->secondChunk = newChunk;
// We're not going to use the extra space in the current chunk...
this->firstChunkLength = this->count;
}
this->length = this->count + newChunk->u.chained.length;
this->lastChunk = newChunk;
this->appendPtr = newChunk->u.chained.buffer;
}
else
{
if (this->initialSize < MaxLength)
{
newLength = max(newLength, this->initialSize + 1);
}
else
{
newLength = MaxLength;
}
Assert(newLength <= MaxLength);
// Let's just grow the current chunk in place
newChunk = this->NewSingleChunk(&newLength);
if (this->count)
{
js_memcpy_s(newChunk->u.single.buffer, newLength * sizeof(char16), this->firstChunk->u.single.buffer, sizeof(char16) * this->count);
}
this->firstChunk = this->lastChunk = newChunk;
this->firstChunkLength = newLength;
this->length = newLength;
this->appendPtr = newChunk->u.single.buffer + this->count;
}
}
void EnsureBuffer(charcount_t countNeeded)
{
if(countNeeded == 0) return;
if (countNeeded >= this->length - this->count)
{
if (countNeeded > MaxLength)
{
// Check upfront to prevent potential uint32 overflow caused by (this->count + countNeeded + 1).
Throw::OutOfMemory();
}
ExtendBuffer(this->count + countNeeded + 1);
}
}
public:
static StringBuilder<TAllocator> *
New(TAllocator* alloc, charcount_t initialSize)
{
if (initialSize > MaxLength)
{
Throw::OutOfMemory();
}
return AllocatorNew(TAllocator, alloc, StringBuilder<TAllocator>, alloc, initialSize);
}
StringBuilder(TAllocator* alloc)
{
new (this) StringBuilder(alloc, 0);
}
StringBuilder(TAllocator* alloc, charcount_t initialSize) : alloc(alloc), length(0), count(0), firstChunk(NULL),
secondChunk(NULL), appendPtr(NULL), initialSize(initialSize)
{
if (initialSize > MaxLength)
{
Throw::OutOfMemory();
}
}
void UnChain(__out __ecount(bufLen) char16 *pBuf, charcount_t bufLen)
{
charcount_t lastChunkCount = this->count;
Assert(this->IsChained());
Assert(bufLen >= this->count);
char16 *pSrcBuf = this->firstChunk->u.single.buffer;
Data *next = this->secondChunk;
charcount_t srcLength = this->firstChunkLength;
for (Data *chunk = this->firstChunk; chunk != this->lastChunk; next = chunk->u.chained.next)
{
if (bufLen < srcLength)
{
Throw::FatalInternalError();
}
js_memcpy_s(pBuf, bufLen * sizeof(char16), pSrcBuf, sizeof(char16) * srcLength);
bufLen -= srcLength;
pBuf += srcLength;
lastChunkCount -= srcLength;
chunk = next;
pSrcBuf = chunk->u.chained.buffer;
srcLength = chunk->u.chained.length;
}
if (bufLen < lastChunkCount)
{
Throw::FatalInternalError();
}
js_memcpy_s(pBuf, bufLen * sizeof(char16), this->lastChunk->u.chained.buffer, sizeof(char16) * lastChunkCount);
}
void UnChain()
{
Assert(this->IsChained());
charcount_t newLength = this->count;
Data *newChunk = this->NewSingleChunk(&newLength);
this->length = newLength;
this->UnChain(newChunk->u.single.buffer, newLength);
this->firstChunk = this->lastChunk = newChunk;
this->secondChunk = NULL;
this->appendPtr = newChunk->u.single.buffer + this->count;
}
void Copy(__out __ecount(bufLen) char16 *pBuf, charcount_t bufLen)
{
if (this->IsChained())
{
this->UnChain(pBuf, bufLen);
}
else
{
if (bufLen < this->count)
{
Throw::FatalInternalError();
}
js_memcpy_s(pBuf, bufLen * sizeof(char16), this->firstChunk->u.single.buffer, this->count * sizeof(char16));
}
}
inline char16* Buffer()
{
if (this->IsChained())
{
this->UnChain();
}
if (this->firstChunk)
{
this->firstChunk->u.single.buffer[this->count] = _u('\0');
return this->firstChunk->u.single.buffer;
}
else
{
return _u("");
}
}
inline charcount_t Count() { return this->count; }
void Append(char16 c)
{
if (this->count == this->length)
{
ExtendBuffer(this->length+1);
}
*(this->appendPtr++) = c;
this->count++;
}
void AppendSz(const char16 * str)
{
// WARNING!!
// Do not use this to append JavascriptStrings. They can have embedded
// nulls which obviously won't be handled correctly here. Instead use
// Append with a length, which will use memcpy and correctly include any
// embedded null characters.
// WARNING!!
while (*str != _u('\0'))
{
Append(*str++);
}
}
void Append(const char16 * str, charcount_t countNeeded)
{
EnsureBuffer(countNeeded);
char16 *dst = this->appendPtr;
JavascriptString::CopyHelper(dst, str, countNeeded);
this->appendPtr += countNeeded;
this->count += countNeeded;
}
template <size_t N>
void AppendCppLiteral(const char16(&str)[N])
{
// Need to account for the terminating null character in C++ string literals, hence N > 2 and N - 1 below
static_assert(N > 2, "Use Append(char16) for appending literal single characters and do not append empty string literal");
Append(str, N - 1);
}
// If we expect str to be large - we should just use this version that uses memcpy directly instead of Append
void AppendLarge(const char16 * str, charcount_t countNeeded)
{
EnsureBuffer(countNeeded);
char16 *dst = this->appendPtr;
js_memcpy_s(dst, sizeof(WCHAR) * countNeeded, str, sizeof(WCHAR) * countNeeded);
this->appendPtr += countNeeded;
this->count += countNeeded;
}
errno_t AppendUint64(unsigned __int64 value)
{
const int max_length = 20; // maximum length of 64-bit value converted to base 10 string
const int radix = 10;
WCHAR buf[max_length+1];
errno_t result = _ui64tow_s(value, buf, max_length+1, radix);
AssertMsg(result==0, "Failed to translate value to string");
if (result == 0)
{
AppendSz(buf);
}
return result;
}
char16 *AllocBufferSpace(charcount_t countNeeded)
{
EnsureBuffer(countNeeded);
return this->appendPtr;
}
void IncreaseCount(charcount_t countInc)
{
if(countInc == 0) return;
this->count += countInc;
this->appendPtr += countInc;
Assert(this->count < this->length);
}
char16* Detach()
{
// NULL terminate the string
Append(_u('\0'));
// if there is a chain we need to account for that also, so that the new buffer will have the NULL at the end.
if (this->IsChained())
{
this->UnChain();
}
// Now decrement the count to adjust according to number of chars
this->count--;
char16* result = this->firstChunk->u.single.buffer;
this->firstChunk = this->lastChunk = NULL;
return result;
}
void TrimTrailingNULL()
{
Assert(this->count);
if (this->IsChained())
{
Assert(this->lastChunk->u.chained.buffer[this->count - (this->length - this->lastChunk->u.chained.length) - 1] == _u('\0'));
}
else
{
Assert(this->lastChunk->u.single.buffer[this->count - 1] == _u('\0'));
}
this->appendPtr--;
this->count--;
}
void Reset()
{
this->length = 0;
this->count = 0;
this->firstChunk = NULL;
this->secondChunk = NULL;
this->lastChunk = NULL;
}
};
}