aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--README.md30
-rw-r--r--config.cfg.dist1
-rw-r--r--src/database_postgres.cc85
-rw-r--r--src/database_sqlite.cc2
-rw-r--r--src/query.cc1
-rw-r--r--src/querymanager.cc192
-rw-r--r--src/querymanager.hh69
8 files changed, 314 insertions, 68 deletions
diff --git a/Makefile b/Makefile
index 0fca315..972092f 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ OUTPUTEXE = querymanager
CC = gcc
CXX = g++
-CFLAGS = -m64 -fno-strict-aliasing -pedantic -Wno-unused-parameter -Wall -Wextra -pthread
+CFLAGS = -m64 -fno-strict-aliasing -pedantic -Wall -Wextra -pthread
CXXFLAGS = $(CFLAGS) --std=c++11
LFLAGS = -Wl,-t
diff --git a/README.md b/README.md
index 170cbb4..5c35257 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,31 @@
# Tibia 7.7 Query Manager
-This is a simple query manager designed to support [Tibia Game Server](https://github.com/fusion32/tibia-game), [Tibia Login Server](https://github.com/fusion32/tibia-login), and [Tibia Web Server](https://github.com/fusion32/tibia-web). It uses the SQLite3 3.50.2 amalgamation for a database backend so we can completely avoid having to spin up yet another service for a separate database system, effectively turning the query manager into the database itself. This design choice was made with a single game server in mind. The networking protocol is NOT encrypted at all and won't accept remote connections. For a fully multi-world distributed infrastructure, a distributed database system like PostgreSQL and MySQL/MariaDB should be considered.
+This is a query manager designed to support [Tibia Game Server](https://github.com/fusion32/tibia-game), [Tibia Login Server](https://github.com/fusion32/tibia-login), and [Tibia Web Server](https://github.com/fusion32/tibia-web). It should be able to support any database system, but only SQLite and PostgreSQL are currently implemented. For SQLite, it uses the SQLite3 3.50.2 amalgamation, which avoids external dependencies and effectivelly turns the query manager into the database itself. For PostgreSQL, you'll need to link against *libpq*, but the query manager turns into a relay to the actual database. The communication protocol is NOT encrypted at all and it's configured to listen to local connections only. If you need a distributed infrastructure, use PostgreSQL!
## Compiling
-Even though there are no Linux specific features being used, it will currently only compile on Linux. It should be simple enough to support compiling on Windows but I don't think it would add any value, considering the game server needs to run on Linux and they need to be both on the same machine. The makefile is very simple and there are **ZERO** dependencies required. You can add the `-j N` switch to make it compile across N processes.
+Currently only Linux is supported. It shouldn't be too difficult to support Windows but I don't think it would add any value, considering the game server is somewhat bound to Linux, and that both need to run on the same machine. The only dependency is *libpq* when using PostgreSQL. The makefile is very simple but there are a few parameters that can be modified to customize compilation:
+- *DEBUG* can be set to a non-zero value to build in debug mode. Defaults to zero.
+- *DATABASE* can be set to either *sqlite* or *postgres* to modify the database system. Defaults to *sqlite*.
+
+Compiling different translation units with different compilation parameters may cause things to blow up so it is recommended to always do a full rebuild. This can be achieved by running `make clean` before compiling or specifying the `-B` option to *make*. Here is a list of recommended commands:
```
-make # build in release mode
-make DEBUG=1 # build in debug mode
-make clean # remove `build` directory
+make -B DEBUG=0 DATABASE=sqlite # build in release mode with SQLite
+make -B DEBUG=1 DATABASE=sqlite # build in debug mode with SQLite
+make -B DEBUG=0 DATABASE=postgres # build in release mode with PostgreSQL
+make -B DEBUG=1 DATABASE=postgres # build in debug mode with PostgreSQL
+make clean # remove `build` directory
```
+## Running (SQLite)
+The query manager becomes the database, automatically initializing and maintaining the schema, based on the files in `sqlite/` (see `sqlite/README.txt`). The default schema file won't automatically insert any initial data (see `sqlite/init.sql`), although you could put some insertions at the end to avoid having to manually run something like `sqlite/init.sql`. There are a few configuration options, and in particular `SQLite.*` options that could be adjusted in `config.cfg` but the defaults should work for most use cases.
+
+## Running (PostgreSQL)
+The query manager becomes a relay to the actual database. And with PostgreSQL being a distributed database system, it makes no sense to have individual clients managing the schema, since there could be multiple, each with their own assumptions. For that reason there is a `SchemaInfo` table with a `VERSION` row that will be queried at startup and compared against `POSTGRESQL_SCHEMA_VERSION`, defined in `src/database_postgres.cc`, to make sure there is an agreement on the schema version. It is hardcoded because schema changes will usually result in query changes.
+
+Aside from having to properly setup and startup a PostgreSQL server (see `postgres/README.txt`), there are quite a few specific extra options (`PostgreSQL.*`) that should be properly configured to be able to reach the database.
+
## Running
-The query manager will automatically manage the database schema based on files in `sql/` (see `sql/README.txt`), but won't automatically insert any initial data (see `sql/init.sql`). It does have a few configuration options that are loaded from `config.cfg` but the defaults should work for most use cases.
+For testing purposes you could simply compile and launch the application from the shell, but if you plan to run the game server on a dedicated machine, it is recommended that it is setup as a service. There is a *systemd* configuration file (`tibia-querymanager.service`) in the repository that may be used for that purpose. The process is very similar to the one described in the [Game Server](https://github.com/fusion32/tibia-game) so I won't repeat myself here.
+
+## Text Encoding
+The original game client and server uses LATIN1 text encoding, which can be a problem if we're assuming strings are UTF8 encoded. For that reason, `TReadBuffer::ReadString` will automatically convert from LATIN1 to UTF8, and `TWriteBuffer::WriteString` will automatically convert from UTF8 to LATIN1, to ensure that LATIN1 is still used as the text encoding of the underlying protocol. This behaviour can be disabled with the `-DCLIENT_ENCODING_UTF8=1` compilation flag but unless you move this encoding bridge to the server-client boundary, you'll have problems. And it's not such a simple task either. Upgrading the game server to UTF8 would require updating game files, changing the script parser, and would ultimately make it incompatible with the leaked game files.
-It is recommended that the query manager is setup as a service. There is a *systemd* configuration file (`tibia-querymanager.service`) in the repository that may be used for that purpose. The process is very similar to the one described in the [Game Server](https://github.com/fusion32/tibia-game) so I won't repeat myself here.
diff --git a/config.cfg.dist b/config.cfg.dist
index 780c50a..dcf7212 100644
--- a/config.cfg.dist
+++ b/config.cfg.dist
@@ -17,7 +17,6 @@ PostgreSQL.DBName = "tibia"
PostgreSQL.User = "tibia"
PostgreSQL.Password = ""
PostgreSQL.ConnectTimeout = ""
-PostgreSQL.ClientEncoding = "UTF8"
PostgreSQL.ApplicationName = "QueryManager"
PostgreSQL.SSLMode = ""
PostgreSQL.SSLRootCert = ""
diff --git a/src/database_postgres.cc b/src/database_postgres.cc
index 7a676e2..80076c9 100644
--- a/src/database_postgres.cc
+++ b/src/database_postgres.cc
@@ -454,6 +454,9 @@ static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int
return Size;
}
+#if 0
+// NOTE(fusion): DO NOT REMOVE. It is currently not being used so it's switched
+// off to avoid compiler warnings.
static int GetResultIPAddress(PGresult *Result, int Row, int Col){
int IPAddress = 0;
int Format = PQfformat(Result, Col);
@@ -524,8 +527,8 @@ static int GetResultIPAddress(PGresult *Result, int Row, int Col){
}
}
return IPAddress;
-
}
+#endif
static bool ParseTimestamp(int *Dest, const char *String){
ASSERT(Dest != NULL && String != NULL);
@@ -633,46 +636,46 @@ static int GetResultTimestamp(PGresult *Result, int Row, int Col){
return Timestamp;
}
-static void SkipWhitespace(int *Cursor, const char *String){
- while(isspace(String[*Cursor])){
- *Cursor += 1;
+static void SkipWhitespace(int *Position, const char *String){
+ while(isspace(String[*Position])){
+ *Position += 1;
}
}
-static bool SkipNextChar(int Ch, int *Cursor, const char *String){
+static bool SkipNextChar(int Ch, int *Position, const char *String){
ASSERT(Ch != 0);
- if(String[*Cursor] != Ch){
+ if(String[*Position] != Ch){
return false;
}
- *Cursor += 1;
+ *Position += 1;
return true;
}
-static bool ReadNextNumber(int *Dest, int *Cursor, const char *String){
- SkipWhitespace(Cursor, String);
+static bool ReadNextNumber(int *Dest, int *Position, const char *String){
+ SkipWhitespace(Position, String);
bool Negative = false;
- if(!isdigit(String[*Cursor])){
- if(String[*Cursor] != '+' && String[*Cursor] != '-'){
+ if(!isdigit(String[*Position])){
+ if(String[*Position] != '+' && String[*Position] != '-'){
return false;
}
- if(!isdigit(String[*Cursor + 1])){
+ if(!isdigit(String[*Position + 1])){
return false;
}
- if(String[*Cursor] == '-'){
+ if(String[*Position] == '-'){
Negative = true;
}
- *Cursor += 1;
+ *Position += 1;
}
int Number = 0;
- while(isdigit(String[*Cursor])){
- Number = (Number * 10) + (String[*Cursor] - '0');
- *Cursor += 1;
+ while(isdigit(String[*Position])){
+ Number = (Number * 10) + (String[*Position] - '0');
+ *Position += 1;
}
if(Negative){
@@ -683,21 +686,21 @@ static bool ReadNextNumber(int *Dest, int *Cursor, const char *String){
return true;
}
-static bool ReadNextWord(char *Dest, int DestCapacity, int *Cursor, const char *String){
+static bool ReadNextWord(char *Dest, int DestCapacity, int *Position, const char *String){
ASSERT(DestCapacity > 0);
- SkipWhitespace(Cursor, String);
- if(!isalpha(String[*Cursor])){
+ SkipWhitespace(Position, String);
+ if(!isalpha(String[*Position])){
return false;
}
int WritePos = 0;
- while(isalpha(String[*Cursor])){
+ while(isalpha(String[*Position])){
if(WritePos < DestCapacity){
- Dest[WritePos] = String[*Cursor];
+ Dest[WritePos] = String[*Position];
WritePos += 1;
}
- *Cursor += 1;
+ *Position += 1;
}
bool Result = (WritePos < DestCapacity);
@@ -716,25 +719,25 @@ static bool ParseInterval(int *Dest, const char *String){
// is stable enough or it would break other client libraries as well.
ASSERT(Dest != NULL && String != NULL);
int Interval = 0;
- int Cursor = 0;
+ int Position = 0;
bool Negate = false;
while(true){
- SkipWhitespace(&Cursor, String);
- if(String[Cursor] == 0){
+ SkipWhitespace(&Position, String);
+ if(String[Position] == 0){
break;
}
int Number;
- if(!ReadNextNumber(&Number, &Cursor, String)){
+ if(!ReadNextNumber(&Number, &Position, String)){
// "Expected number"
return false;
}
- if(SkipNextChar(':', &Cursor, String)){
+ if(SkipNextChar(':', &Position, String)){
int Minutes, Seconds;
- if(!ReadNextNumber(&Minutes, &Cursor, String)
- || !SkipNextChar(':', &Cursor, String)
- || !ReadNextNumber(&Seconds, &Cursor, String)){
+ if(!ReadNextNumber(&Minutes, &Position, String)
+ || !SkipNextChar(':', &Position, String)
+ || !ReadNextNumber(&Seconds, &Position, String)){
// "Expected HH:MM:SS.FFFFFF"
return false;
}
@@ -750,15 +753,15 @@ static bool ParseInterval(int *Dest, const char *String){
}
// NOTE(fusion): Parse microseconds but ignore it.
- if(SkipNextChar('.', &Cursor, String)){
+ if(SkipNextChar('.', &Position, String)){
int Frac;
- int PrevCursor = Cursor;
- if(!ReadNextNumber(&Frac, &Cursor, String)){
+ int PrevPosition = Position;
+ if(!ReadNextNumber(&Frac, &Position, String)){
// "Expected fractional part"
return false;
}
- int FracDigits = Cursor - PrevCursor;
+ int FracDigits = Position - PrevPosition;
if(FracDigits > 6){
// "Too many fractional digits"
return false;
@@ -768,7 +771,7 @@ static bool ParseInterval(int *Dest, const char *String){
Interval += (Number * 3600 + Minutes * 60 + Seconds);
}else{
char Unit[16];
- if(!ReadNextWord(Unit, sizeof(Unit), &Cursor, String)){
+ if(!ReadNextWord(Unit, sizeof(Unit), &Position, String)){
// "Expected unit"
return false;
}
@@ -813,14 +816,14 @@ static bool ParseInterval(int *Dest, const char *String){
}
char Direction[8];
- if(ReadNextWord(Direction, sizeof(Direction), &Cursor, String)){
+ if(ReadNextWord(Direction, sizeof(Direction), &Position, String)){
if(!StringEqCI(Direction, "ago")){
// "Invalid interval direction"
return false;
}
- SkipWhitespace(&Cursor, String);
- if(String[Cursor] != 0){
+ SkipWhitespace(&Position, String);
+ if(String[Position] != 0){
// "Interval direction is expected only at the very end"
return false;
}
@@ -1184,10 +1187,10 @@ TDatabase *DatabaseOpen(void){
"user",
"password",
"connect_timeout",
- "client_encoding",
"application_name",
"sslmode",
"sslrootcert",
+ "client_encoding",
NULL, // sentinel
};
@@ -1198,10 +1201,10 @@ TDatabase *DatabaseOpen(void){
g_Config.PostgreSQL.User,
g_Config.PostgreSQL.Password,
g_Config.PostgreSQL.ConnectTimeout,
- g_Config.PostgreSQL.ClientEncoding,
g_Config.PostgreSQL.ApplicationName,
g_Config.PostgreSQL.SSLMode,
g_Config.PostgreSQL.SSLRootCert,
+ "UTF8",
NULL, // sentinel
};
diff --git a/src/database_sqlite.cc b/src/database_sqlite.cc
index 8523854..5b3bbc0 100644
--- a/src/database_sqlite.cc
+++ b/src/database_sqlite.cc
@@ -855,7 +855,7 @@ bool CharacterNameExists(TDatabase *Database, const char *Name, bool *Exists){
}
*Exists = (ErrorCode == SQLITE_ROW);
- return false;
+ return true;
}
bool CreateCharacter(TDatabase *Database, int WorldID, int AccountID, const char *Name, int Sex){
diff --git a/src/query.cc b/src/query.cc
index c4d81a1..afcf664 100644
--- a/src/query.cc
+++ b/src/query.cc
@@ -1346,6 +1346,7 @@ void ProcessCancelHouseTransfer(TDatabase *Database, TQuery *Query){
// are kept permanently and this query is used to delete/flag it, in case
// the it didn't complete. We might need to refine `FinishHouseTransfers`.
//int HouseID = Buffer->Read16();
+ (void)Database;
QueryOk(Query);
}
diff --git a/src/querymanager.cc b/src/querymanager.cc
index 5719504..86c2e47 100644
--- a/src/querymanager.cc
+++ b/src/querymanager.cc
@@ -136,6 +136,16 @@ int RoundSecondsToDays(int Seconds){
return (Seconds + 86399) / 86400;
}
+uint32 HashString(const char *String){
+ // FNV1a 32-bits
+ uint32 Hash = 0x811C9DC5U;
+ for(int i = 0; String[i] != 0; i += 1){
+ Hash ^= (uint32)String[i];
+ Hash *= 0x01000193U;
+ }
+ return Hash;
+}
+
bool StringEmpty(const char *String){
return String[0] == 0;
}
@@ -231,14 +241,178 @@ bool StringFormat(char *Dest, int DestCapacity, const char *Format, ...){
return Written >= 0 && Written < DestCapacity;
}
-uint32 HashString(const char *String){
- // FNV1a 32-bits
- uint32 Hash = 0x811C9DC5U;
- for(int i = 0; String[i] != 0; i += 1){
- Hash ^= (uint32)String[i];
- Hash *= 0x01000193U;
+int UTF8SequenceSize(uint8 LeadingByte){
+ if((LeadingByte & 0x80) == 0){
+ return 1;
+ }else if((LeadingByte & 0xE0) == 0xC0){
+ return 2;
+ }else if((LeadingByte & 0xF0) == 0xE0){
+ return 3;
+ }else if((LeadingByte & 0xF8) == 0xF0){
+ return 4;
+ }else{
+ return 0;
}
- return Hash;
+}
+
+bool UTF8IsTrailingByte(uint8 Byte){
+ return (Byte & 0xC0) == 0x80;
+}
+
+int UTF8EncodedSize(int Codepoint){
+ if(Codepoint < 0){
+ return 0;
+ }else if(Codepoint <= 0x7F){
+ return 1;
+ }else if(Codepoint <= 0x07FF){
+ return 2;
+ }else if(Codepoint <= 0xFFFF){
+ return 3;
+ }else if(Codepoint <= 0x10FFFF){
+ return 4;
+ }else{
+ return 0;
+ }
+}
+
+int UTF8FindNextLeadingByte(const char *Src, int SrcLength){
+ int Offset = 0;
+ while(Offset < SrcLength){
+ // NOTE(fusion): Allow the first byte to be a leading byte, in case we
+ // just want to advance from one leading byte to another.
+ if(Offset > 0 && !UTF8IsTrailingByte(Src[Offset])){
+ break;
+ }
+ Offset += 1;
+ }
+ return Offset;
+}
+
+int UTF8DecodeOne(const uint8 *Src, int SrcLength, int *OutCodepoint){
+ if(SrcLength <= 0){
+ return 0;
+ }
+
+ int Size = UTF8SequenceSize(Src[0]);
+ if(Size <= 0 || Size > SrcLength){
+ return 0;
+ }
+
+ for(int i = 1; i < Size; i += 1){
+ if(!UTF8IsTrailingByte(Src[i])){
+ return 0;
+ }
+ }
+
+ int Codepoint = 0;
+ switch(Size){
+ case 1:{
+ Codepoint = (int)Src[0];
+ break;
+ }
+
+ case 2:{
+ Codepoint = ((int)(Src[0] & 0x1F) << 6)
+ | ((int)(Src[1] & 0x3F) << 0);
+ break;
+ }
+
+ case 3:{
+ Codepoint = ((int)(Src[0] & 0x0F) << 12)
+ | ((int)(Src[1] & 0x3F) << 6)
+ | ((int)(Src[2] & 0x3F) << 0);
+ break;
+ }
+
+ case 4:{
+ Codepoint = ((int)(Src[0] & 0x07) << 18)
+ | ((int)(Src[1] & 0x3F) << 12)
+ | ((int)(Src[2] & 0x3F) << 6)
+ | ((int)(Src[3] & 0x3F) << 0);
+ break;
+ }
+ }
+
+ if(OutCodepoint){
+ *OutCodepoint = Codepoint;
+ }
+
+ return Size;
+}
+
+int UTF8EncodeOne(uint8 *Dest, int DestCapacity, int Codepoint){
+ int Size = UTF8EncodedSize(Codepoint);
+ if(Size > 0 && Size <= DestCapacity){
+ switch(Size){
+ case 1:{
+ Dest[0] = (uint8)Codepoint;
+ break;
+ }
+
+ case 2:{
+ Dest[0] = (uint8)(0xC0 | (0x1F & (Codepoint >> 6)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+
+ case 3:{
+ Dest[0] = (uint8)(0xE0 | (0x0F & (Codepoint >> 12)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 6)));
+ Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+
+ case 4:{
+ Dest[0] = (uint8)(0xF0 | (0x07 & (Codepoint >> 18)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 12)));
+ Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 6)));
+ Dest[3] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+ }
+ }
+
+ return Size;
+}
+
+// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather
+// convert any characters, INCLUDING the null-terminator, contained in the src
+// string. Invalid or NON-LATIN1 codepoints are translated into '?'.
+int UTF8ToLatin1(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ int ReadPos = 0;
+ int WritePos = 0;
+ while(ReadPos < SrcLength){
+ int Codepoint = -1;
+ int Size = UTF8DecodeOne((uint8*)(Src + ReadPos), (SrcLength - ReadPos), &Codepoint);
+ if(Size > 0){
+ ReadPos += Size;
+ }else{
+ ReadPos += UTF8FindNextLeadingByte((Src + ReadPos), (SrcLength - ReadPos));
+ }
+
+ if(WritePos < DestCapacity){
+ if(Codepoint >= 0 && Codepoint <= 0xFF){
+ Dest[WritePos] = (char)Codepoint;
+ }else{
+ Dest[WritePos] = '?';
+ }
+ }
+ WritePos += 1;
+ }
+
+ return WritePos;
+}
+
+// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather
+// convert any characters, INCLUDING the null-terminator, contained in the src
+// string. Note that LATIN1 characters translates directly into UNICODE codepoints.
+int Latin1ToUTF8(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ int WritePos = 0;
+ for(int ReadPos = 0; ReadPos < SrcLength; ReadPos += 1){
+ WritePos += UTF8EncodeOne((uint8*)(Dest + WritePos),
+ (DestCapacity - WritePos), (uint8)Src[ReadPos]);
+ }
+ return WritePos;
}
int HexDigit(int Ch){
@@ -509,8 +683,6 @@ bool ReadConfig(const char *FileName, TConfig *Config){
ParseStringBuf(Config->PostgreSQL.Password, Val);
}else if(StringEqCI(Key, "PostgreSQL.ConnectTimeout")){
ParseStringBuf(Config->PostgreSQL.ConnectTimeout, Val);
- }else if(StringEqCI(Key, "PostgreSQL.ClientEncoding")){
- ParseStringBuf(Config->PostgreSQL.ClientEncoding, Val);
}else if(StringEqCI(Key, "PostgreSQL.ApplicationName")){
ParseStringBuf(Config->PostgreSQL.ApplicationName, Val);
}else if(StringEqCI(Key, "PostgreSQL.SSLMode")){
@@ -601,7 +773,6 @@ int main(int argc, const char **argv){
StringBufCopy(g_Config.PostgreSQL.User, "tibia");
StringBufCopy(g_Config.PostgreSQL.Password, "");
StringBufCopy(g_Config.PostgreSQL.ConnectTimeout, "");
- StringBufCopy(g_Config.PostgreSQL.ClientEncoding, "UTF8");
StringBufCopy(g_Config.PostgreSQL.ApplicationName, "QueryManager");
StringBufCopy(g_Config.PostgreSQL.SSLMode, "");
StringBufCopy(g_Config.PostgreSQL.SSLRootCert, "");
@@ -642,7 +813,6 @@ int main(int argc, const char **argv){
LOG("PostgreSQL dbname: \"%s\"", g_Config.PostgreSQL.DBName);
LOG("PostgreSQL user: \"%s\"", g_Config.PostgreSQL.User);
LOG("PostgreSQL connect_timeout: \"%s\"", g_Config.PostgreSQL.ConnectTimeout);
- LOG("PostgreSQL client_encoding: \"%s\"", g_Config.PostgreSQL.ClientEncoding);
LOG("PostgreSQL application_name: \"%s\"", g_Config.PostgreSQL.ApplicationName);
LOG("PostgreSQL sslmode: \"%s\"", g_Config.PostgreSQL.SSLMode);
LOG("PostgreSQL sslrootcert: \"%s\"", g_Config.PostgreSQL.SSLRootCert);
diff --git a/src/querymanager.hh b/src/querymanager.hh
index 1b90a0d..e33bcc0 100644
--- a/src/querymanager.hh
+++ b/src/querymanager.hh
@@ -111,7 +111,6 @@ struct TConfig{
char User[30];
char Password[30];
char ConnectTimeout[30];
- char ClientEncoding[30];
char ApplicationName[30];
char SSLMode[30];
char SSLRootCert[100];
@@ -153,6 +152,7 @@ void SleepMS(int DurationMS);
void CryptoRandom(uint8 *Buffer, int Count);
int RoundSecondsToDays(int Seconds);
+uint32 HashString(const char *String);
bool StringEmpty(const char *String);
bool StringEq(const char *A, const char *B);
bool StringEqCI(const char *A, const char *B);
@@ -162,7 +162,14 @@ bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength);
bool StringCopy(char *Dest, int DestCapacity, const char *Src);
void StringCopyEllipsis(char *Dest, int DestCapacity, const char *Src);
bool StringFormat(char *Dest, int DestCapacity, const char *Format, ...) ATTR_PRINTF(3, 4);
-uint32 HashString(const char *String);
+int UTF8SequenceSize(uint8 LeadingByte);
+bool UTF8IsTrailingByte(uint8 Byte);
+int UTF8EncodedSize(int Codepoint);
+int UTF8FindNextLeadingByte(const char *Src, int SrcLength);
+int UTF8DecodeOne(const uint8 *Src, int SrcLength, int *OutCodepoint);
+int UTF8EncodeOne(uint8 *Dest, int DestCapacity, int Codepoint);
+int UTF8ToLatin1(char *Dest, int DestCapacity, const char *Src, int SrcLength);
+int Latin1ToUTF8(char *Dest, int DestCapacity, const char *Src, int SrcLength);
int HexDigit(int Ch);
int ParseHexString(uint8 *Dest, int DestCapacity, const char *String);
@@ -391,6 +398,7 @@ struct TReadBuffer{
return Result;
}
+#if CLIENT_ENCODING_UTF8
void ReadString(char *Dest, int DestCapacity){
int Length = (int)this->Read16();
if(Length == 0xFFFF){
@@ -398,16 +406,39 @@ struct TReadBuffer{
}
if(Dest != NULL && DestCapacity > 0){
- if(Length < DestCapacity && this->CanRead(Length)){
+ int Written = 0;
+ if(this->CanRead(Length) && Length < DestCapacity){
memcpy(Dest, this->Buffer + this->Position, Length);
- Dest[Length] = 0;
- }else{
- Dest[0] = 0;
+ Written = Length;
}
+ memset((Dest + Written), 0, (DestCapacity - Written));
+ }
+
+ this->Position += Length;
+ }
+#else
+ void ReadString(char *Dest, int DestCapacity){
+ int Length = (int)this->Read16();
+ if(Length == 0xFFFF){
+ Length = (int)this->Read32();
+ }
+
+ if(Dest != NULL && DestCapacity > 0){
+ int Written = 0;
+ if(this->CanRead(Length)){
+ const char *Src = (const char*)(this->Buffer + this->Position);
+ Written = Latin1ToUTF8(Dest, DestCapacity, Src, Length);
+ if(Written >= DestCapacity){
+ Written = 0;
+ }
+ }
+
+ memset((Dest + Written), 0, (DestCapacity - Written));
}
this->Position += Length;
}
+#endif
};
struct TWriteBuffer{
@@ -466,6 +497,7 @@ struct TWriteBuffer{
this->Position += 4;
}
+#if CLIENT_ENCODING_UTF8
void WriteString(const char *String){
int StringLength = 0;
if(String != NULL){
@@ -485,6 +517,31 @@ struct TWriteBuffer{
this->Position += StringLength;
}
+#else
+ void WriteString(const char *String){
+ int StringLength = 0;
+ int OutputLength = 0;
+ if(String != NULL){
+ StringLength = (int)strlen(String);
+ OutputLength = UTF8ToLatin1(NULL, 0, String, (int)strlen(String));
+ }
+
+ if(OutputLength < 0xFFFF){
+ this->Write16((uint16)OutputLength);
+ }else{
+ this->Write16(0xFFFF);
+ this->Write32((uint32)OutputLength);
+ }
+
+ if(OutputLength > 0 && this->CanWrite(OutputLength)){
+ int Written = UTF8ToLatin1((char*)(this->Buffer + this->Position),
+ (this->Size - this->Position), String, StringLength);
+ ASSERT(Written == OutputLength);
+ }
+
+ this->Position += OutputLength;
+ }
+#endif
void Rewrite16(int Position, uint16 Value){
if((Position + 2) <= this->Position && !this->Overflowed()){