mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 10:40:20 +03:00
Optimize eterm Kitty graphics rendering
- bulk-ingest Kitty APC payloads without per-byte UTF-8 processing - preserve APC buffer capacity between graphics commands - update existing GPU textures when replacing video frames - validate Base64 while decoding directly into transfer storage - reserve known image sizes and share immutable pixel buffers - coalesce superseded anonymous video-frame updates - support POSIX shared-memory transmission with stable snapshots - fix large unchunked images and anonymous placement replacement - add fragmentation, malformed input, video, and shared-memory tests - update Kitty graphics protocol documentation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#include <eepp/system/base64.hpp>
|
||||
#include <array>
|
||||
#include <eepp/system/base64.hpp>
|
||||
|
||||
namespace EE { namespace System {
|
||||
|
||||
@@ -106,6 +106,46 @@ size_t decodeBase64( size_t in_len, const char* in, size_t out_len, unsigned cha
|
||||
return io;
|
||||
}
|
||||
|
||||
size_t decodeBase64Strict( size_t inLen, const char* input, size_t outLen, unsigned char* output ) {
|
||||
size_t outputOffset = 0;
|
||||
Uint32 value = 0;
|
||||
unsigned bits = 0;
|
||||
size_t padding = 0;
|
||||
for ( size_t offset = 0; offset < inLen; ++offset ) {
|
||||
const Uint8 decoded = base64dec_tab[static_cast<unsigned char>( input[offset] )];
|
||||
if ( decoded == BASE64_PADDING ) {
|
||||
padding = inLen - offset;
|
||||
if ( padding > 2 || inLen % 4 != 0 )
|
||||
return static_cast<size_t>( -1 );
|
||||
for ( size_t remainder = offset; remainder < inLen; ++remainder )
|
||||
if ( input[remainder] != '=' )
|
||||
return static_cast<size_t>( -1 );
|
||||
break;
|
||||
}
|
||||
if ( decoded > 63 || padding != 0 )
|
||||
return static_cast<size_t>( -1 );
|
||||
value = ( value << 6 ) | decoded;
|
||||
bits += 6;
|
||||
if ( bits >= 8 ) {
|
||||
bits -= 8;
|
||||
if ( outputOffset >= outLen )
|
||||
return static_cast<size_t>( -1 );
|
||||
output[outputOffset++] = static_cast<unsigned char>( ( value >> bits ) & 0xFF );
|
||||
if ( bits == 0 )
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
const size_t dataLength = inLen - padding;
|
||||
if ( dataLength % 4 == 1 || ( padding == 1 && dataLength % 4 != 3 ) ||
|
||||
( padding == 2 && dataLength % 4 != 2 ) )
|
||||
return static_cast<size_t>( -1 );
|
||||
// Reject non-canonical encodings whose unused bits are non-zero. Besides being strict, this
|
||||
// avoids accepting multiple byte strings for the same payload at protocol boundaries.
|
||||
if ( bits != 0 && ( value & ( ( 1u << bits ) - 1u ) ) != 0 )
|
||||
return static_cast<size_t>( -1 );
|
||||
return outputOffset;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) {
|
||||
@@ -114,6 +154,8 @@ size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned c
|
||||
|
||||
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out,
|
||||
DecodeMode mode ) {
|
||||
if ( mode == DecodeMode::NoWhitespaceStrict )
|
||||
return decodeBase64Strict( in_len, in, out_len, out );
|
||||
return mode == DecodeMode::NoWhitespace ? decodeBase64<false>( in_len, in, out_len, out )
|
||||
: decodeBase64<true>( in_len, in, out_len, out );
|
||||
}
|
||||
@@ -165,8 +207,8 @@ bool Base64::encode( std::string_view in, std::string& out ) {
|
||||
if ( out.size() < b64len )
|
||||
out.resize( b64len );
|
||||
|
||||
const size_t len =
|
||||
encode( in.size(), reinterpret_cast<const unsigned char*>( in.data() ), out.size(), out.data() );
|
||||
const size_t len = encode( in.size(), reinterpret_cast<const unsigned char*>( in.data() ),
|
||||
out.size(), out.data() );
|
||||
|
||||
if ( len != static_cast<size_t>( -1 ) && len != out.size() )
|
||||
out.resize( len );
|
||||
|
||||
@@ -35,6 +35,7 @@ struct KittyGraphicsCommandData {
|
||||
std::string_view payload;
|
||||
std::optional<Uint32> format;
|
||||
std::optional<Uint32> dataSize;
|
||||
std::optional<Uint32> dataOffset;
|
||||
std::optional<Uint32> more;
|
||||
std::optional<Uint32> imageId;
|
||||
std::optional<Uint32> imageNumber;
|
||||
@@ -167,7 +168,7 @@ class KittyGraphicsProtocol {
|
||||
Int32 gapMs{ 40 };
|
||||
Uint32 usageHint{ 0 };
|
||||
};
|
||||
std::vector<Uint8> rgba;
|
||||
std::shared_ptr<std::vector<Uint8>> rgba;
|
||||
std::unordered_map<Uint32, Frame> frames;
|
||||
Sizei size;
|
||||
Uint32 imageNumber{ 0 };
|
||||
|
||||
@@ -9,8 +9,16 @@
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
#if EE_PLATFORM != EE_PLATFORM_WIN && EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
using namespace EE::System;
|
||||
|
||||
namespace eterm { namespace Terminal {
|
||||
@@ -54,40 +62,96 @@ bool checkedPixelBytes( Uint32 width, Uint32 height, size_t channels, size_t& re
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validBase64( std::string_view input, bool finalChunk ) {
|
||||
if ( input.size() % 4 == 1 )
|
||||
bool decodeBase64( std::string_view input, bool finalChunk, std::vector<Uint8>& output ) {
|
||||
if ( !finalChunk && input.size() % 4 != 0 )
|
||||
return false;
|
||||
const size_t oldSize = output.size();
|
||||
const size_t capacity = Base64::decodeSafeOutLen( input.size() );
|
||||
constexpr size_t MaxTransferBytes = 64 * 1024 * 1024;
|
||||
if ( oldSize > MaxTransferBytes || capacity > MaxTransferBytes - oldSize )
|
||||
return false;
|
||||
output.resize( oldSize + capacity );
|
||||
const size_t decodedSize =
|
||||
Base64::decode( input.size(), input.data(), capacity, output.data() + oldSize,
|
||||
Base64::DecodeMode::NoWhitespaceStrict );
|
||||
if ( decodedSize == static_cast<size_t>( -1 ) ) {
|
||||
output.resize( oldSize );
|
||||
return false;
|
||||
size_t padding = 0;
|
||||
for ( size_t i = 0; i < input.size(); ++i ) {
|
||||
const unsigned char character = input[i];
|
||||
const bool alphabet =
|
||||
( character >= 'A' && character <= 'Z' ) || ( character >= 'a' && character <= 'z' ) ||
|
||||
( character >= '0' && character <= '9' ) || character == '+' || character == '/';
|
||||
if ( character == '=' ) {
|
||||
++padding;
|
||||
if ( !finalChunk || padding > 2 )
|
||||
return false;
|
||||
} else if ( !alphabet || padding != 0 ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return finalChunk || input.size() % 4 == 0;
|
||||
output.resize( oldSize + decodedSize );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeBase64( std::string_view input, bool finalChunk, std::vector<Uint8>& output ) {
|
||||
if ( !validBase64( input, finalChunk ) )
|
||||
bool readSharedMemory( const KittyGraphicsCommandData& data, std::vector<Uint8>& output ) {
|
||||
#if EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
|
||||
(void)data;
|
||||
(void)output;
|
||||
return false;
|
||||
#else
|
||||
std::vector<Uint8> decodedName;
|
||||
if ( !decodeBase64( data.payload, true, decodedName ) || decodedName.empty() ||
|
||||
decodedName.size() > 255 ||
|
||||
std::find( decodedName.begin() + 1, decodedName.end(), '/' ) != decodedName.end() ||
|
||||
std::find( decodedName.begin(), decodedName.end(), 0 ) != decodedName.end() )
|
||||
return false;
|
||||
std::vector<Uint8> decoded( Base64::decodeSafeOutLen( input.size() ) );
|
||||
const size_t decodedSize = Base64::decode( input.size(), input.data(), decoded.size(),
|
||||
decoded.data(), Base64::DecodeMode::NoWhitespace );
|
||||
if ( decodedSize == static_cast<size_t>( -1 ) )
|
||||
std::string name( decodedName.begin(), decodedName.end() );
|
||||
const int descriptor = shm_open( name.c_str(), O_RDONLY, 0 );
|
||||
if ( descriptor == -1 )
|
||||
return false;
|
||||
decoded.resize( decodedSize );
|
||||
constexpr size_t MaxTransferBytes = 64 * 1024 * 1024;
|
||||
if ( output.size() > MaxTransferBytes || decoded.size() > MaxTransferBytes - output.size() )
|
||||
return false;
|
||||
output.insert( output.end(), decoded.begin(), decoded.end() );
|
||||
return true;
|
||||
// POSIX Kitty transfers are single-use. Unlink immediately after opening so all error paths
|
||||
// still retire the client-owned object while the descriptor keeps its contents alive.
|
||||
shm_unlink( name.c_str() );
|
||||
struct stat status{};
|
||||
const size_t offset = data.dataOffset.value_or( 0 );
|
||||
bool valid = fstat( descriptor, &status ) == 0 && status.st_size >= 0 &&
|
||||
static_cast<Uint64>( status.st_size ) >= offset;
|
||||
size_t bytes = 0;
|
||||
if ( valid ) {
|
||||
const size_t available = static_cast<size_t>( status.st_size ) - offset;
|
||||
bytes = data.dataSize.value_or( static_cast<Uint32>(
|
||||
std::min<size_t>( available, std::numeric_limits<Uint32>::max() ) ) );
|
||||
valid = bytes <= available && bytes <= 128 * 1024 * 1024;
|
||||
}
|
||||
if ( valid && bytes != 0 ) {
|
||||
const size_t mappingBytes = offset + bytes;
|
||||
valid = mappingBytes >= bytes && mappingBytes <= 128 * 1024 * 1024;
|
||||
void* mapping = valid ? mmap( nullptr, mappingBytes, PROT_READ, MAP_SHARED, descriptor, 0 )
|
||||
: MAP_FAILED;
|
||||
if ( mapping == MAP_FAILED ) {
|
||||
valid = false;
|
||||
} else {
|
||||
const auto* source = static_cast<const Uint8*>( mapping ) + offset;
|
||||
// mpv reuses the same shm name for every frame. If it reopened the object before
|
||||
// we unlinked it above, its next memcpy can overlap this read. Require consecutive
|
||||
// identical observations after yielding to the writer; otherwise retain the previous
|
||||
// displayed frame instead of publishing visibly torn rows.
|
||||
std::vector<Uint8> snapshot( source, source + bytes );
|
||||
bool stable = false;
|
||||
unsigned stableObservations = 0;
|
||||
constexpr unsigned RequiredStableObservations = 2;
|
||||
constexpr unsigned MaxSnapshotAttempts = 6;
|
||||
for ( unsigned attempt = 0; attempt < MaxSnapshotAttempts; ++attempt ) {
|
||||
std::this_thread::yield();
|
||||
if ( std::memcmp( snapshot.data(), source, bytes ) == 0 ) {
|
||||
if ( ++stableObservations == RequiredStableObservations ) {
|
||||
stable = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
stableObservations = 0;
|
||||
snapshot.assign( source, source + bytes );
|
||||
}
|
||||
}
|
||||
if ( stable )
|
||||
output = std::move( snapshot );
|
||||
else
|
||||
valid = false;
|
||||
munmap( mapping, mappingBytes );
|
||||
}
|
||||
}
|
||||
close( descriptor );
|
||||
return valid && bytes != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool placementContains( const TerminalVisiblePlacement& placement, Vector2i cell ) {
|
||||
@@ -147,6 +211,9 @@ KittyGraphicsParseResult KittyGraphicsProtocol::parse( std::string_view command
|
||||
case 'S':
|
||||
PARSE_UINT_FIELD( dataSize );
|
||||
break;
|
||||
case 'O':
|
||||
PARSE_UINT_FIELD( dataOffset );
|
||||
break;
|
||||
case 'm':
|
||||
PARSE_UINT_FIELD( more );
|
||||
valid = valid && unsignedValue <= 1;
|
||||
@@ -327,6 +394,20 @@ KittyGraphicsHandleResult
|
||||
KittyGraphicsProtocol::handleTransmit( const KittyGraphicsCommandData& data, bool display,
|
||||
bool query, bool frame, Vector2i cursor ) {
|
||||
const bool more = data.more.value_or( 0 ) != 0;
|
||||
if ( data.transmission == 's' ) {
|
||||
if ( mPending.active )
|
||||
mPending = {};
|
||||
PendingTransfer transfer;
|
||||
transfer.data = data;
|
||||
transfer.data.payload = {};
|
||||
transfer.display = display;
|
||||
transfer.query = query;
|
||||
transfer.frame = frame;
|
||||
if ( !readSharedMemory( data, transfer.decodedData ) )
|
||||
return { response( data, KittyGraphicsError::DecodeFailed ),
|
||||
KittyGraphicsError::DecodeFailed, false };
|
||||
return finishTransfer( std::move( transfer ), cursor );
|
||||
}
|
||||
if ( mPending.active ) {
|
||||
if ( frame != mPending.frame || data.format || data.dataSize || data.imageId ||
|
||||
data.imageNumber || data.usageHint || data.placementId || data.width || data.height ||
|
||||
@@ -361,6 +442,14 @@ KittyGraphicsProtocol::handleTransmit( const KittyGraphicsCommandData& data, boo
|
||||
transfer.query = query;
|
||||
transfer.frame = frame;
|
||||
transfer.active = true;
|
||||
const Uint32 format = data.format.value_or( 32 );
|
||||
if ( format == 24 || format == 32 ) {
|
||||
size_t expectedBytes = 0;
|
||||
if ( data.width && data.height &&
|
||||
checkedPixelBytes( *data.width, *data.height, format == 24 ? 3 : 4, expectedBytes ) &&
|
||||
expectedBytes <= 64 * 1024 * 1024 )
|
||||
transfer.decodedData.reserve( expectedBytes );
|
||||
}
|
||||
if ( !decodeBase64( data.payload, !more, transfer.decodedData ) )
|
||||
return { response( data, KittyGraphicsError::InvalidData ), KittyGraphicsError::InvalidData,
|
||||
false };
|
||||
@@ -495,7 +584,9 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
|
||||
std::vector<Uint8>* destinationPixels = nullptr;
|
||||
bool createdFrame = false;
|
||||
if ( frameNumber == 1 ) {
|
||||
destinationPixels = &image->second.rgba;
|
||||
if ( !image->second.rgba.unique() )
|
||||
image->second.rgba = std::make_shared<std::vector<Uint8>>( *image->second.rgba );
|
||||
destinationPixels = image->second.rgba.get();
|
||||
} else {
|
||||
auto frame = image->second.frames.find( frameNumber );
|
||||
if ( frame == image->second.frames.end() ) {
|
||||
@@ -505,7 +596,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
|
||||
if ( data.columns ) {
|
||||
const Uint32 baseFrame = *data.columns;
|
||||
if ( baseFrame == 1 )
|
||||
newFrame.rgba = image->second.rgba;
|
||||
newFrame.rgba = *image->second.rgba;
|
||||
else {
|
||||
auto base = image->second.frames.find( baseFrame );
|
||||
if ( base == image->second.frames.end() )
|
||||
@@ -620,7 +711,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
|
||||
false };
|
||||
|
||||
auto existing = mImages.find( imageId );
|
||||
const size_t oldBytes = existing == mImages.end() ? 0 : existing->second.rgba.size();
|
||||
const size_t oldBytes = existing == mImages.end() ? 0 : existing->second.rgba->size();
|
||||
if ( !ensureCapacity( pixels.size(), imageId, existing == mImages.end() ) )
|
||||
return { response( data, KittyGraphicsError::NoSpace ), KittyGraphicsError::NoSpace,
|
||||
false };
|
||||
@@ -646,8 +737,8 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
|
||||
image.usageHint = data.usageHint.value_or( 0 );
|
||||
image.anonymous = anonymous;
|
||||
image.creationSerial = ++mCreationSerial;
|
||||
image.rgba = std::move( pixels );
|
||||
mStorageBytes = mStorageBytes - oldBytes + image.rgba.size();
|
||||
image.rgba = std::make_shared<std::vector<Uint8>>( std::move( pixels ) );
|
||||
mStorageBytes = mStorageBytes - oldBytes + image.rgba->size();
|
||||
const bool replaced = existing != mImages.end();
|
||||
auto inserted = mImages.insert_or_assign( imageId, std::move( image ) ).first;
|
||||
|
||||
@@ -658,7 +749,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
|
||||
update.imageSize = inserted->second.size;
|
||||
update.region =
|
||||
Rect( 0, 0, inserted->second.size.getWidth(), inserted->second.size.getHeight() );
|
||||
update.rgba = std::make_shared<const std::vector<Uint8>>( inserted->second.rgba );
|
||||
update.rgba = inserted->second.rgba;
|
||||
mUpdates.emplace_back( std::move( update ) );
|
||||
++mStats.fullImageUpdates;
|
||||
++mPresentationGeneration;
|
||||
@@ -1122,7 +1213,7 @@ KittyGraphicsProtocol::composeFrames( const KittyGraphicsCommandData& data ) {
|
||||
false };
|
||||
auto pixelsFor = [&]( Uint32 frameNumber ) -> std::vector<Uint8>* {
|
||||
if ( frameNumber == 1 )
|
||||
return &image->second.rgba;
|
||||
return image->second.rgba.get();
|
||||
auto frame = image->second.frames.find( frameNumber );
|
||||
return frame == image->second.frames.end() ? nullptr : &frame->second.rgba;
|
||||
};
|
||||
@@ -1162,6 +1253,10 @@ KittyGraphicsProtocol::composeFrames( const KittyGraphicsCommandData& data ) {
|
||||
source->data() + static_cast<size_t>( sourceY + row ) * imageStride +
|
||||
static_cast<size_t>( sourceX ) * 4,
|
||||
rowBytes );
|
||||
if ( destinationFrame == 1 && !image->second.rgba.unique() ) {
|
||||
image->second.rgba = std::make_shared<std::vector<Uint8>>( *image->second.rgba );
|
||||
destination = image->second.rgba.get();
|
||||
}
|
||||
const bool replace = data.cursorMovement.value_or( 0 ) == 1;
|
||||
for ( Uint32 row = 0; row < height; ++row ) {
|
||||
Uint8* target = destination->data() +
|
||||
@@ -1239,7 +1334,7 @@ void KittyGraphicsProtocol::eraseImage( KittyImageId imageId ) {
|
||||
auto image = mImages.find( imageId );
|
||||
if ( image == mImages.end() )
|
||||
return;
|
||||
mStorageBytes -= image->second.rgba.size();
|
||||
mStorageBytes -= image->second.rgba->size();
|
||||
for ( const auto& frame : image->second.frames )
|
||||
mFrameStorageBytes -= frame.second.rgba.size();
|
||||
mImages.erase( image );
|
||||
@@ -1252,7 +1347,7 @@ void KittyGraphicsProtocol::eraseImage( KittyImageId imageId ) {
|
||||
bool KittyGraphicsProtocol::ensureCapacity( size_t bytes, KittyImageId replacingId,
|
||||
bool addingImage ) {
|
||||
auto replaced = mImages.find( replacingId );
|
||||
const size_t replacedBytes = replaced == mImages.end() ? 0 : replaced->second.rgba.size();
|
||||
const size_t replacedBytes = replaced == mImages.end() ? 0 : replaced->second.rgba->size();
|
||||
auto hasCapacity = [&] {
|
||||
return bytes <= mMaxStorageBytes &&
|
||||
mStorageBytes - replacedBytes <= mMaxStorageBytes - bytes &&
|
||||
@@ -1401,7 +1496,7 @@ std::shared_ptr<TerminalGraphicsPresentation> KittyGraphicsProtocol::takePresent
|
||||
|
||||
const std::vector<Uint8>* KittyGraphicsProtocol::imagePixels( KittyImageId imageId ) const {
|
||||
auto image = mImages.find( imageId );
|
||||
return image == mImages.end() ? nullptr : &image->second.rgba;
|
||||
return image == mImages.end() ? nullptr : image->second.rgba.get();
|
||||
}
|
||||
|
||||
bool KittyGraphicsProtocol::hasVirtualPlacements() const {
|
||||
@@ -1539,7 +1634,7 @@ void KittyGraphicsProtocol::resync() {
|
||||
create.imageId = image.first;
|
||||
create.imageSize = image.second.size;
|
||||
create.region = Rect( 0, 0, image.second.size.getWidth(), image.second.size.getHeight() );
|
||||
create.rgba = std::make_shared<const std::vector<Uint8>>( image.second.rgba );
|
||||
create.rgba = image.second.rgba;
|
||||
mUpdates.emplace_back( std::move( create ) );
|
||||
for ( const auto& frame : image.second.frames ) {
|
||||
TerminalGraphicsUpdate createFrame;
|
||||
|
||||
@@ -26,8 +26,22 @@ bool KittyGraphicsRenderer::applyUpdates( std::vector<TerminalGraphicsUpdate>&&
|
||||
return false;
|
||||
|
||||
switch ( update.type ) {
|
||||
case TerminalGraphicsUpdateType::CreateImage:
|
||||
case TerminalGraphicsUpdateType::ReplaceImage: {
|
||||
if ( !update.rgba || update.imageSize.getWidth() <= 0 ||
|
||||
update.imageSize.getHeight() <= 0 )
|
||||
return false;
|
||||
auto existing = mImages.find( update.imageId );
|
||||
if ( existing != mImages.end() && existing->second.texture &&
|
||||
existing->second.size == update.imageSize ) {
|
||||
existing->second.texture->update( update.rgba->data(),
|
||||
update.imageSize.getWidth(),
|
||||
update.imageSize.getHeight(), 0, 0 );
|
||||
existing->second.frames.clear();
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case TerminalGraphicsUpdateType::CreateImage: {
|
||||
if ( !update.rgba || update.imageSize.getWidth() <= 0 ||
|
||||
update.imageSize.getHeight() <= 0 )
|
||||
return false;
|
||||
|
||||
@@ -2664,10 +2664,16 @@ void TerminalEmulator::strdump( void ) {
|
||||
}
|
||||
|
||||
void TerminalEmulator::strreset( void ) {
|
||||
auto old = mStrescseq.buf;
|
||||
char* buffer = mStrescseq.buf;
|
||||
size_t capacity = mStrescseq.siz;
|
||||
constexpr size_t MaxRetainedStringCapacity = 128 * 1024;
|
||||
if ( !buffer || capacity > MaxRetainedStringCapacity ) {
|
||||
buffer = (char*)xrealloc( buffer, STR_BUF_SIZ );
|
||||
capacity = STR_BUF_SIZ;
|
||||
}
|
||||
mStrescseq = STREscape{};
|
||||
mStrescseq.buf = (char*)xrealloc( old, STR_BUF_SIZ );
|
||||
mStrescseq.siz = STR_BUF_SIZ;
|
||||
mStrescseq.buf = buffer;
|
||||
mStrescseq.siz = capacity;
|
||||
}
|
||||
|
||||
void TerminalEmulator::sendbreak( const TerminalArg* ) {
|
||||
@@ -3230,6 +3236,45 @@ int TerminalEmulator::twrite( const char* buf, int buflen, int show_ctrl ) {
|
||||
int n;
|
||||
|
||||
for ( n = 0; n < buflen; n += charsize ) {
|
||||
/* Kitty control data and payload are ASCII transport bytes. Once ESC _ G has been
|
||||
* recognized, append ordinary bytes in bulk instead of routing every Base64 byte through
|
||||
* UTF-8 decoding and the terminal character state machine. Control bytes remain on the
|
||||
* normal path so fragmented ESC \\ termination and malformed strings retain their exact
|
||||
* behavior. */
|
||||
if ( !show_ctrl && ( mTerm.esc & ESC_STR ) && mStrescseq.type == '_' &&
|
||||
mStrescseq.len > 0 && mStrescseq.buf[0] == 'G' ) {
|
||||
int end = n;
|
||||
while ( end < buflen ) {
|
||||
const unsigned char byte = static_cast<unsigned char>( buf[end] );
|
||||
if ( byte == '\a' || byte == 030 || byte == 032 || byte == 033 ||
|
||||
( byte >= 0x80 && byte <= 0x9F ) )
|
||||
break;
|
||||
++end;
|
||||
}
|
||||
const size_t bytes = static_cast<size_t>( end - n );
|
||||
if ( bytes != 0 ) {
|
||||
if ( !mStrescseq.discarded ) {
|
||||
if ( mStrescseq.len > MAX_KITTY_GRAPHICS_APC_SIZE ||
|
||||
bytes > MAX_KITTY_GRAPHICS_APC_SIZE - mStrescseq.len ) {
|
||||
mStrescseq.discarded = true;
|
||||
} else {
|
||||
const size_t required = mStrescseq.len + bytes + 1;
|
||||
if ( required > mStrescseq.siz ) {
|
||||
size_t capacity = mStrescseq.siz;
|
||||
while ( capacity < required )
|
||||
capacity = eemin( capacity * 2, MAX_KITTY_GRAPHICS_APC_SIZE + 1 );
|
||||
mStrescseq.buf = (char*)xrealloc( mStrescseq.buf, capacity );
|
||||
mStrescseq.siz = capacity;
|
||||
}
|
||||
std::memcpy( mStrescseq.buf + mStrescseq.len, buf + n, bytes );
|
||||
mStrescseq.len += bytes;
|
||||
}
|
||||
}
|
||||
n = end;
|
||||
if ( n == buflen )
|
||||
return buflen;
|
||||
}
|
||||
}
|
||||
if ( IS_SET( MODE_UTF8 ) ) {
|
||||
/* process a complete utf8 char */
|
||||
charsize = utf8decode( buf + n, &u, buflen - n );
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "utest.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <eepp/system/base64.hpp>
|
||||
#include <eepp/system/compression.hpp>
|
||||
#include <eepp/system/iostreammemory.hpp>
|
||||
@@ -13,6 +14,12 @@
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
|
||||
#if EE_PLATFORM != EE_PLATFORM_WIN && EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
using namespace eterm::Terminal;
|
||||
using namespace eterm::System;
|
||||
using namespace EE::System;
|
||||
@@ -23,6 +30,7 @@ class MockPty : public IPseudoTerminal {
|
||||
std::string mWrites;
|
||||
bool mLoopWrites{ true };
|
||||
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
|
||||
std::deque<size_t> mReadSizes;
|
||||
size_t mReadOffset{ 0 };
|
||||
std::atomic<size_t> mBytesRead{ 0 };
|
||||
int mCols = 80;
|
||||
@@ -48,7 +56,12 @@ class MockPty : public IPseudoTerminal {
|
||||
int read( char* buf, size_t n, bool ) override {
|
||||
if ( mReadOffset == mBuffer.size() )
|
||||
return 0;
|
||||
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, mMaxRead } );
|
||||
size_t readLimit = mMaxRead;
|
||||
if ( !mReadSizes.empty() ) {
|
||||
readLimit = mReadSizes.front();
|
||||
mReadSizes.pop_front();
|
||||
}
|
||||
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, readLimit } );
|
||||
memcpy( buf, mBuffer.data() + mReadOffset, toRead );
|
||||
mReadOffset += toRead;
|
||||
mBytesRead.fetch_add( toRead, std::memory_order_relaxed );
|
||||
@@ -935,6 +948,62 @@ UTEST( eterm, kitty_graphics_apc_is_fragmentation_safe_and_not_terminal_text ) {
|
||||
EXPECT_EQ( static_cast<Rune>( 'K' ), display->mSecondGlyph.u );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_bulk_apc_accepts_every_input_split_boundary ) {
|
||||
const std::string stream = "\033_Ga=T,f=32,s=1,v=1,q=2;AQIDBA==\033\\";
|
||||
for ( size_t split = 1; split < stream.size(); ++split ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = stream;
|
||||
pty->mLoopWrites = false;
|
||||
pty->mReadSizes = { split, stream.size() - split };
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term =
|
||||
TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
term->update();
|
||||
while ( !term->update() ) {
|
||||
}
|
||||
ASSERT_TRUE( display->mGraphics != nullptr );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), display->mGraphics->placements.size() );
|
||||
}
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_strict_base64_rejects_invalid_payload_bytes ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidData,
|
||||
protocol.handle( "a=t,f=32,s=1,v=1;AQI BA==" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidData,
|
||||
protocol.handle( "a=t,f=32,s=1,v=1;AQIDBA=$" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidData,
|
||||
protocol.handle( "a=t,f=32,s=1,v=1;AQ=DBA==" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidData, protocol.handle( "a=t,f=32,s=1,v=1;AB==" ).error );
|
||||
}
|
||||
|
||||
#if EE_PLATFORM != EE_PLATFORM_WIN && EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
|
||||
UTEST( eterm, kitty_graphics_reads_and_unlinks_posix_shared_memory ) {
|
||||
// mpv uses the Linux-compatible form without the optional leading slash.
|
||||
const std::string name = "eterm-kitty-unit-" + std::to_string( getpid() );
|
||||
shm_unlink( name.c_str() );
|
||||
const int descriptor = shm_open( name.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600 );
|
||||
ASSERT_TRUE( descriptor >= 0 );
|
||||
const Uint8 stored[] = { 99, 98, 1, 2, 3 };
|
||||
ASSERT_EQ( static_cast<ssize_t>( sizeof( stored ) ),
|
||||
write( descriptor, stored, sizeof( stored ) ) );
|
||||
close( descriptor );
|
||||
|
||||
std::string encodedName;
|
||||
ASSERT_TRUE( Base64::encode( name, encodedName ) );
|
||||
KittyGraphicsProtocol protocol;
|
||||
EXPECT_EQ( KittyGraphicsError::None,
|
||||
protocol.handle( "a=T,t=s,f=24,s=1,v=1,O=2,S=3,q=2,m=1;" + encodedName ).error );
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
ASSERT_TRUE( updates[0].rgba != nullptr );
|
||||
const std::vector<Uint8> expected{ 1, 2, 3, 255 };
|
||||
EXPECT_TRUE( expected == *updates[0].rgba );
|
||||
EXPECT_EQ( -1, shm_open( name.c_str(), O_RDONLY, 0 ) );
|
||||
}
|
||||
#endif
|
||||
|
||||
UTEST( eterm, kitty_graphics_accepts_unchunked_direct_image_larger_than_eight_kibibytes ) {
|
||||
std::vector<Uint8> rgb( 64 * 64 * 3 );
|
||||
for ( size_t offset = 0; offset < rgb.size(); offset += 3 )
|
||||
|
||||
Reference in New Issue
Block a user