diff --git a/api/src/c/api-client.c b/api/src/c/api-client.c index 5045050c46..fc545ea3ba 100644 --- a/api/src/c/api-client.c +++ b/api/src/c/api-client.c @@ -1,6 +1,8 @@ #include "api-client.h" #include #include +#include +#include // Max number of base64 bytes to encode a geo point. #define MAPSWITHME_MAX_POINT_BYTES 10 @@ -70,10 +72,113 @@ void MapsWithMe_LatLonToString(double lat, double lon, char * s, int nBytes) } } -int MapsWithMe_GenShortShowMapUrl(double lat, double lon, double zoomLevel, char const * name, char * buf, int bufSize) +// Do special URL Encoding: +// URL restricted / unsafe / unwise characters are %-encoded. +// See rfc3986, rfc1738, rfc2396. +// ' ' is replaced with '_' +// '_' is replaces with %-encoded space, i.e. %20 +int MapsWithMe_UrlEncodeString(char const * s, int size, char ** res) +{ + *res = malloc(size * 3 + 1); + char * out = *res; + for (int i = 0; i < size; ++i) + { + unsigned char c = (unsigned char)(s[i]); + switch (c) + { + case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: + case 0x08: case 0x09: case 0x0A: case 0x0B: case 0x0C: case 0x0D: case 0x0E: case 0x0F: + case 0x10: case 0x11: case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: + case 0x18: case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: case 0x1E: case 0x1F: + case 0x7F: + case '<': + case '>': + case '#': + case '%': + case '"': + case '!': + case '*': + case '\'': + case '(': + case ')': + case ';': + case ':': + case '@': + case '&': + case '=': + case '+': + case '$': + case ',': + case '/': + case '?': + case '[': + case ']': + case '{': + case '}': + case '|': + case '^': + case '`': + *(out++) = '%'; + *(out++) = "0123456789ABCDEF"[c >> 4]; + *(out++) = "0123456789ABCDEF"[c & 15]; + break; + case ' ': + *(out++) = '_'; + break; + case '_': + *(out++) = '%'; *(out++) = '2'; *(out++) = '0'; + break; + default: + *(out++) = s[i]; + } + } + *(out++) = 0; + return out - *res - 1; +} + +void MapsWithMe_AppendString(char * buf, int bufSize, int * bytesAppended, char const * s, int size) +{ + int const bytesAvailable = bufSize - *bytesAppended; + if (bytesAvailable > 0) + memcpy(buf + *bytesAppended, s, size < bytesAvailable ? size : bytesAvailable); + + *bytesAppended += size; +} + +int MapsWithMe_GenShortShowMapUrl(double lat, double lon, double zoom, char const * name, char * buf, int bufSize) { // @TODO: Implement MapsWithMe_GenShortShowMapUrl(). - // @TODO: Escape URL-unfriendly characters: ! * ' ( ) ; : @ & = + $ , / ? % # [ ] - return 0; + // URL format: + // + // +------------------ 1 byte: zoom level + // |+-------+--------- 9 bytes: lat,lon + // || | +--+---- Variable number of bytes: point name + // || | | | + // ge0://ZCoordba64/Name + + int fullUrlSize = 0; + + char urlPrefix[] = "ge0://ZCoord6789"; + + int const zoomI = (zoom <= 4 ? 0 : (zoom >= 19.75 ? 63 : (int) ((zoom - 4) * 4))); + urlPrefix[6] = MapsWithMe_Base64Char(zoomI); + + MapsWithMe_LatLonToString(lat, lon, urlPrefix + 7, 9); + + MapsWithMe_AppendString(buf, bufSize, &fullUrlSize, urlPrefix, 16); + + if (name != 0 && name[0] != 0) + { + MapsWithMe_AppendString(buf, bufSize, &fullUrlSize, "/", 1); + + char * encName; + int const encNameSize = MapsWithMe_UrlEncodeString(name, strlen(name), &encName); + + MapsWithMe_AppendString(buf, bufSize, &fullUrlSize, encName, encNameSize); + + free(encName); + } + + return fullUrlSize; } diff --git a/api/tests/c/api-client-test.c b/api/tests/c/api-client-test.c index ba74e164c1..49c7960eef 100644 --- a/api/tests/c/api-client-test.c +++ b/api/tests/c/api-client-test.c @@ -249,5 +249,179 @@ FCT_BGN() fct_chk((max2 - min2) * 1.0 / max2 < 0.05); } FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_SmokeTest) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Name", buf, 100); + fct_chk_eq_str("ge0://8wAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_NameIsNull) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, 0, buf, 100); + fct_chk_eq_str("ge0://8wAAAAAAAA", buf); + fct_chk_eq_int(16, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_NameIsEmpty) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "", buf, 100); + fct_chk_eq_str("ge0://8wAAAAAAAA", buf); + fct_chk_eq_int(16, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_ZoomVerySmall) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 2, "Name", buf, 100); + fct_chk_eq_str("ge0://AwAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_ZoomNegative) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, -5, "Name", buf, 100); + fct_chk_eq_str("ge0://AwAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_ZoomLarge) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 20, "Name", buf, 100); + fct_chk_eq_str("ge0://_wAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_ZoomVeryLarge) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 2000000000, "Name", buf, 100); + fct_chk_eq_str("ge0://_wAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_FractionalZoom) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 8.25, "Name", buf, 100); + fct_chk_eq_str("ge0://RwAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_FractionalZoomRoundsDown) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 8.499, "Name", buf, 100); + fct_chk_eq_str("ge0://RwAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_FractionalZoomNextStep) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 8.5, "Name", buf, 100); + fct_chk_eq_str("ge0://SwAAAAAAAA/Name", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_SpaceIsReplacedWithUnderscore) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Hello World", buf, 100); + fct_chk_eq_str("ge0://8wAAAAAAAA/Hello_World", buf); + fct_chk_eq_int(28, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_NamesAreEscaped) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "'Hello,World!%$", buf, 100); + fct_chk_eq_str("ge0://8wAAAAAAAA/%27Hello%2CWorld%21%25%24", buf); + fct_chk_eq_int(42, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_UnderscoreIsReplacedWith%20) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Hello_World", buf, 100); + fct_chk_eq_str("ge0://8wAAAAAAAA/Hello%20World", buf); + fct_chk_eq_int(30, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_ControlCharsAreEscaped) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Hello\tWorld\n", buf, 100); + fct_chk_eq_str("ge0://8wAAAAAAAA/Hello%09World%0A", buf); + fct_chk_eq_int(33, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_BufferNullAndEmpty) + { + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Name", 0, 0); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_BufferNotNullAndEmpty) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Name", buf, 0); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_TerminatingNullIsNotWritten) + { + char buf[] = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Name", buf, 27); + fct_chk_eq_str("ge0://8wAAAAAAAA/Namexxxxxx", buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_BufferIs1Byte) + { + char buf; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Name", &buf, 1); + fct_chk_eq_int('g', buf); + fct_chk_eq_int(21, res); + } + FCT_QTEST_END(); + + FCT_QTEST_BGN(MapsWithMe_GenShortShowMapUrl_BufferTooSmall) + { + for (int bufSize = 1; bufSize <= 21; ++bufSize) + { + char buf[100] = {0}; + int res = MapsWithMe_GenShortShowMapUrl(0, 0, 19, "Name", buf, bufSize); + char expected[] = "ge0://8wAAAAAAAA/Name"; + expected[bufSize] = 0; + fct_chk_eq_str(expected, buf); + fct_chk_eq_int(21, res); + } + } + FCT_QTEST_END(); + } FCT_END();