blob: 280a2f5c949288408b7fe1ebd873be288cccc386 [file] [log] [blame]
kkostiuk74d1ae42021-06-17 11:10:15 -04001/*
2 * Copyright (C) 2021-2022 Savoir-faire Linux Inc.
3 *
4 * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21#import "Adapter.h"
22#import "Utils.h"
23#import "jamiNotificationExtension-Swift.h"
24
25#import "jami/jami.h"
26#import "jami/configurationmanager_interface.h"
27#import "jami/callmanager_interface.h"
28#import "jami/conversation_interface.h"
29#import "jami/datatransfer_interface.h"
30
31#define MSGPACK_DISABLE_LEGACY_NIL
32#import "opendht/crypto.h"
33#import "opendht/default_types.h"
kkostiuke10e6572022-07-26 15:41:12 -040034#import "yaml-cpp/yaml.h"
kkostiuk74d1ae42021-06-17 11:10:15 -040035
36#import "json/json.h"
37#import "fstream"
38#import "charconv"
39
40@implementation Adapter
41
42static id<AdapterDelegate> _delegate;
43
44using namespace DRing;
45
46struct PeerConnectionRequest : public dht::EncryptedValue<PeerConnectionRequest>
47{
48 static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA;
49 static constexpr const char* key_prefix = "peer:";
50 dht::Value::Id id = dht::Value::INVALID_ID;
51 std::string ice_msg {};
52 bool isAnswer {false};
53 std::string connType {};
54 MSGPACK_DEFINE_MAP(id, ice_msg, isAnswer, connType)
55};
56
57typedef NS_ENUM(NSInteger, NotificationType) { videoCall, audioCall, gitMessage, unknown };
58
59// Constants
60const std::string fileSeparator = "/";
61NSString* const certificates = @"certificates";
62NSString* const crls = @"crls";
63NSString* const ocsp = @"ocsp";
kkostiuke10e6572022-07-26 15:41:12 -040064NSString* const nameCache = @"namecache";
65NSString* const defaultNameServer = @"ns.jami.net";
66std::string const nameServerConfiguration = "RingNS.uri";
67NSString* const accountConfig = @"config.yml";
kkostiuk74d1ae42021-06-17 11:10:15 -040068
69std::map<std::string, std::shared_ptr<CallbackWrapperBase>> confHandlers;
kkostiuke10e6572022-07-26 15:41:12 -040070std::map<std::string, std::string> cachedNames;
71std::map<std::string, std::string> nameServers;
kkostiuk74d1ae42021-06-17 11:10:15 -040072
73#pragma mark Callbacks registration
74- (void)registerSignals
75{
76 confHandlers.insert(exportable_callback<ConfigurationSignal::GetAppDataPath>(
77 [&](const std::string& name, std::vector<std::string>* ret) {
78 if (name == "cache") {
79 auto path = [Constants cachesPath];
80 ret->push_back(std::string([path.path UTF8String]));
81 } else {
82 auto path = [Constants documentsPath];
83 ret->push_back(std::string([path.path UTF8String]));
84 }
85 }));
86
87 confHandlers.insert(exportable_callback<ConversationSignal::MessageReceived>(
88 [&](const std::string& accountId,
89 const std::string& conversationId,
90 std::map<std::string, std::string> message) {
91 if (Adapter.delegate) {
92 NSString* convId = [NSString stringWithUTF8String:conversationId.c_str()];
93 NSString* account = [NSString stringWithUTF8String:accountId.c_str()];
94 NSMutableDictionary* interaction = [Utils mapToDictionnary:message];
95 [Adapter.delegate newInteractionWithConversationId:convId
96 accountId:account
97 message:interaction];
98 }
99 }));
100
101 confHandlers.insert(exportable_callback<DataTransferSignal::DataTransferEvent>(
102 [&](const std::string& account_id,
103 const std::string& conversation_id,
104 const std::string& interaction_id,
105 const std::string& file_id,
106 int eventCode) {
107 if (Adapter.delegate) {
108 NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
109 NSString* conversationId = [NSString stringWithUTF8String:conversation_id.c_str()];
110 NSString* fileId = [NSString stringWithUTF8String:file_id.c_str()];
111 NSString* interactionId = [NSString stringWithUTF8String:interaction_id.c_str()];
112 [Adapter.delegate dataTransferEventWithFileId:fileId
113 withEventCode:eventCode
114 accountId:accountId
115 conversationId:conversationId
116 interactionId:interactionId];
117 }
118 }));
119
120 confHandlers.insert(exportable_callback<ConversationSignal::ConversationSyncFinished>(
121 [&](const std::string& account_id) {
122 if (Adapter.delegate) {
123 NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
124 [Adapter.delegate conversationSyncCompletedWithAccountId:accountId];
125 }
126 }));
127
128 confHandlers.insert(exportable_callback<ConversationSignal::CallConnectionRequest>(
129 [&](const std::string& account_id, const std::string& peer_id, bool hasVideo) {
130 if (Adapter.delegate) {
131 NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
132 NSString* peerId = [NSString stringWithUTF8String:peer_id.c_str()];
133 [Adapter.delegate receivedCallConnectionRequestWithAccountId:accountId
134 peerId:peerId
135 hasVideo:hasVideo];
136 }
137 }));
138 registerSignalHandlers(confHandlers);
139}
140
141#pragma mark AdapterDelegate
142+ (id<AdapterDelegate>)delegate
143{
144 return _delegate;
145}
146
147+ (void)setDelegate:(id<AdapterDelegate>)delegate
148{
149 _delegate = delegate;
150}
151
152- (bool)downloadFileWithFileId:(NSString*)fileId
153 accountId:(NSString*)accountId
154 conversationId:(NSString*)conversationId
155 interactionId:(NSString*)interactionId
156 withFilePath:(NSString*)filePath
157{
158 return downloadFile(std::string([accountId UTF8String]),
159 std::string([conversationId UTF8String]),
160 std::string([interactionId UTF8String]),
161 std::string([fileId UTF8String]),
162 std::string([filePath UTF8String]));
163}
164
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400165- (BOOL)start:(NSString*)accountId
kkostiuk74d1ae42021-06-17 11:10:15 -0400166{
167 [self registerSignals];
168 if (DRing::initialized() == true) {
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400169 setAccountActive(std::string([accountId UTF8String]), true);
kkostiuk74d1ae42021-06-17 11:10:15 -0400170 return true;
171 }
172#if DEBUG
173 int flag = DRing::DRING_FLAG_CONSOLE_LOG | DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_IOS_EXTENSION;
174#else
175 int flag = DRing::DRING_FLAG_IOS_EXTENSION;;
176#endif
177 if (![[NSThread currentThread] isMainThread]) {
178 __block bool success;
179 dispatch_sync(dispatch_get_main_queue(), ^{
180 if (init(static_cast<DRing::InitFlag>(flag))) {
181 success = start({});
182 } else {
183 success = false;
184 }
185 });
186 return success;
187 } else {
188 if (init(static_cast<DRing::InitFlag>(flag))) {
189 return start({});
190 }
191 return false;
192 }
193}
194
195- (void)stop
196{
197 unregisterSignalHandlers();
198 confHandlers.clear();
199 [self setAccountsActive:false];
200}
201
202- (void)setAccountsActive:(BOOL)active
203{
204 auto accounts = getAccountList();
205 for (auto account : accounts) {
kkostiuk39672932022-07-12 11:36:30 -0400206 setAccountActive(account, active, true);
kkostiuk74d1ae42021-06-17 11:10:15 -0400207 }
208}
209
210- (NSDictionary<NSString*, NSString*>*)decrypt:(NSString*)keyPath
211 treated:(NSString*)treatedMessagesPath
212 value:(NSDictionary*)value
213{
214 if (![[NSFileManager defaultManager] fileExistsAtPath:keyPath]) {
215 return {};
216 }
217
218 NSData* data = [[NSFileManager defaultManager] contentsAtPath:keyPath];
219 const uint8_t* bytes = (const uint8_t*) [data bytes];
220 dht::crypto::PrivateKey dhtKey(bytes, [data length], "");
221
222 Json::Value jsonValue = toJson(value);
223 dht::Value dhtValue(jsonValue);
224
225 if (!dhtValue.isEncrypted()) {
226 return {};
227 }
228 try {
kkostiuk03fa94c2022-07-14 16:30:35 -0400229 dht::Sp<dht::Value> decrypted = dhtValue.decrypt(dhtKey);
230 auto unpacked = msgpack::unpack((const char*) decrypted->data.data(), decrypted->data.size());
kkostiuk74d1ae42021-06-17 11:10:15 -0400231 auto peerCR = unpacked.get().as<PeerConnectionRequest>();
232 if (isMessageTreated(peerCR.id, [treatedMessagesPath UTF8String])) {
233 return {};
234 }
kkostiuk03fa94c2022-07-14 16:30:35 -0400235 auto certPath = [[Constants documentsPath] URLByAppendingPathComponent:certificates].path.UTF8String;
kkostiuk74d1ae42021-06-17 11:10:15 -0400236 auto crlPath = [[Constants documentsPath] URLByAppendingPathComponent:crls].path.UTF8String;
237 auto ocspPath = [[Constants documentsPath] URLByAppendingPathComponent:ocsp].path.UTF8String;
kkostiuk03fa94c2022-07-14 16:30:35 -0400238 std::string peerId = getPeerId(decrypted->owner->getId().toString(),
kkostiuk74d1ae42021-06-17 11:10:15 -0400239 certPath,
240 crlPath,
241 ocspPath);
242 return @{@(peerId.c_str()): @(peerCR.connType.c_str())};
243 } catch (std::runtime_error error) {
244 }
245 return {};
246}
247
kkostiuke10e6572022-07-26 15:41:12 -0400248-(NSString*)getNameFor:(NSString*)address accountId:(NSString*)accountId {
249 return @(getName(std::string([address UTF8String]), std::string([accountId UTF8String])).c_str());
250}
251
252-(NSString*)nameServerForAccountId:(NSString*)accountId; {
253 auto nameServer = getNameServer(std::string([accountId UTF8String]));
254 return nameServer.empty() ? defaultNameServer : @(nameServer.c_str());
255}
256
kkostiuk74d1ae42021-06-17 11:10:15 -0400257Json::Value
258toJson(NSDictionary* value)
259{
260 Json::Value val;
261 for (NSString* key in value.allKeys) {
262 if ([[value objectForKey:key] isKindOfClass:[NSString class]]) {
263 NSString* stringValue = [value objectForKey:key];
264 val[key.UTF8String] = stringValue.UTF8String;
265 } else if ([[value objectForKey:key] isKindOfClass:[NSNumber class]]) {
266 NSNumber* number = [value objectForKey:key];
267 if ([key isEqualToString:@"id"]) {
268 unsigned long long int intValue = [number unsignedLongLongValue];
269 val[key.UTF8String] = intValue;
270 } else {
271 int intValue = [number intValue];
272 val[key.UTF8String] = intValue;
273 }
274 }
275 }
276 return val;
277}
278
kkostiuke10e6572022-07-26 15:41:12 -0400279std::string getName(std::string addres, std::string accountId)
280{
281 auto name = cachedNames.find(addres);
282 if (name != cachedNames.end()) {
283 return name->second;
284 }
285
286 auto ns = getNameServer(accountId);
287 NSString* nameServer = ns.empty() ? defaultNameServer : @(ns.c_str());
288 std::string namesPath = [[[Constants cachesPath] URLByAppendingPathComponent: nameCache] URLByAppendingPathComponent: nameServer].path.UTF8String;
289
290 msgpack::unpacker pac;
291 // read file
292 std::ifstream file = std::ifstream(namesPath, std::ios_base::in);
293 if (!file.is_open()) {
294 return "";
295 }
296 std::string line;
297 while (std::getline(file, line)) {
298 pac.reserve_buffer(line.size());
299 memcpy(pac.buffer(), line.data(), line.size());
300 pac.buffer_consumed(line.size());
301 }
302
303 // load values
304 msgpack::object_handle oh;
305 if (pac.next(oh))
306 oh.get().convert(cachedNames);
307 auto cacheRes = cachedNames.find(addres);
308 return cacheRes != cachedNames.end() ? cacheRes->second : std::string {};
309}
310
311std::string getNameServer(std::string accountId) {
312 auto it = nameServers.find(accountId);
313 if (it != nameServers.end()) {
314 return it->second;
315 }
316 std::string nameServer {};
317 auto accountConfigPath = [[[Constants documentsPath] URLByAppendingPathComponent: @(accountId.c_str())] URLByAppendingPathComponent: accountConfig].path.UTF8String;
318 try {
319 std::ifstream file = std::ifstream(accountConfigPath, std::ios_base::in);
320 YAML::Node node = YAML::Load(file);
321 file.close();
322 nameServer = node[nameServerConfiguration].as<std::string>();
323 if (!nameServer.empty()) {
324 nameServers.insert(std::pair<std::string, std::string>(accountId, nameServer));
325 }
326 } catch (const std::exception& e) {}
327 return nameServer;
328}
329
kkostiuk74d1ae42021-06-17 11:10:15 -0400330#pragma mark functions copied from the daemon
331
332#define LIKELY(expr) (expr)
333#define UNLIKELY(expr) (expr)
334
335/*
336 * Check whether a Unicode (5.2) char is in a valid range.
337 *
338 * The first check comes from the Unicode guarantee to never encode
339 * a point above 0x0010ffff, since UTF-16 couldn't represent it.
340 *
341 * The second check covers surrogate pairs (category Cs).
342 *
343 * @param Char the character
344 */
345#define UNICODE_VALID(Char) ((Char) < 0x110000 && (((Char) &0xFFFFF800) != 0xD800))
346
347#define CONTINUATION_CHAR \
348 if ((*(unsigned char*) p & 0xc0) != 0x80) /* 10xxxxxx */ \
349 goto error; \
350 val <<= 6; \
351 val |= (*(unsigned char*) p) & 0x3f;
352
kkostiuk74d1ae42021-06-17 11:10:15 -0400353template<typename ID = dht::Value::Id>
354bool
355isMessageTreated(ID messageId, const std::string& path)
356{
357 std::ifstream file = std::ifstream(path, std::ios_base::in);
358 if (!file.is_open()) {
359 return false;
360 }
361 std::set<ID, std::less<>> treatedMessages;
362 std::string line;
363 while (std::getline(file, line)) {
364 if constexpr (std::is_same<ID, std::string>::value) {
365 treatedMessages.emplace(std::move(line));
366 } else if constexpr (std::is_integral<ID>::value) {
367 ID vid;
368 if (auto [p, ec] = std::from_chars(line.data(), line.data() + line.size(), vid, 16);
369 ec == std::errc()) {
370 treatedMessages.emplace(vid);
371 }
372 }
373 }
374 return treatedMessages.find(messageId) != treatedMessages.end();
375}
376
377std::string
378getPeerId(const std::string& key,
379 const std::string& certPath,
380 const std::string& crlPath,
381 const std::string& ocspPath)
382{
383 std::map<std::string, std::shared_ptr<dht::crypto::Certificate>> certs;
384 auto dir_content = readDirectory(certPath);
385 unsigned n = 0;
386 for (const auto& f : dir_content) {
387 try {
388 auto crt = std::make_shared<dht::crypto::Certificate>(
389 loadFile(certPath + fileSeparator + f));
390 auto id = crt->getId().toString();
391 auto longId = crt->getLongId().toString();
392 if (id != f && longId != f)
393 throw std::logic_error("Certificate id mismatch");
394 while (crt) {
395 id = crt->getId().toString();
396 longId = crt->getLongId().toString();
397 certs.emplace(std::move(id), crt);
398 certs.emplace(std::move(longId), crt);
399 loadRevocations(*crt, crlPath, ocspPath);
400 crt = crt->issuer;
401 ++n;
402 }
403 } catch (const std::exception& e) {
404 }
405 }
406 auto cit = certs.find(key);
407 if (cit == certs.cend()) {
408 return {};
409 }
410 dht::InfoHash peer_account_id;
411 if (not foundPeerDevice(cit->second, peer_account_id)) {
412 return {};
413 }
414 return peer_account_id.toString();
415}
416
417void
418loadRevocations(dht::crypto::Certificate& crt,
419 const std::string& crlPath,
420 const std::string& ocspPath)
421{
422 auto dir = crlPath + fileSeparator + crt.getId().toString();
423 for (const auto& crl : readDirectory(dir)) {
424 try {
425 crt.addRevocationList(
426 std::make_shared<dht::crypto::RevocationList>(loadFile(dir + fileSeparator + crl)));
427 } catch (const std::exception& e) {
428 }
429 }
430 auto ocsp_dir = ocspPath + fileSeparator + crt.getId().toString();
431 for (const auto& ocsp : readDirectory(ocsp_dir)) {
432 try {
433 std::string ocsp_filepath = ocsp_dir + fileSeparator + ocsp;
434 auto serial = crt.getSerialNumber();
435 if (dht::toHex(serial.data(), serial.size()) != ocsp)
436 continue;
437 dht::Blob ocspBlob = loadFile(ocsp_filepath);
438 crt.ocspResponse = std::make_shared<dht::crypto::OcspResponse>(ocspBlob.data(),
439 ocspBlob.size());
440 } catch (const std::exception& e) {
441 }
442 }
443}
444
445bool
446foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& crt, dht::InfoHash& account_id)
447{
448 if (not crt)
449 return false;
450
451 auto top_issuer = crt;
452 while (top_issuer->issuer)
453 top_issuer = top_issuer->issuer;
454
455 if (top_issuer == crt) {
456 return false;
457 }
458 dht::crypto::TrustList peer_trust;
459 peer_trust.add(*top_issuer);
460 if (not peer_trust.verify(*crt)) {
461 return false;
462 }
463 if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD) {
464 return false;
465 }
466 account_id = crt->issuer->getId();
467 return true;
468}
469
470std::vector<std::string>
471readDirectory(const std::string& dir)
472{
473 NSError* error;
474 NSFileManager* fileMgr = [NSFileManager defaultManager];
475 NSArray* files = [fileMgr contentsOfDirectoryAtPath:@(dir.c_str()) error:&error];
476
477 std::vector<std::string> vector;
478 for (NSString* fileName in files) {
479 vector.push_back([fileName UTF8String]);
480 }
481 return vector;
482}
483
484std::vector<uint8_t>
485loadFile(const std::string& path)
486{
487 if (![[NSFileManager defaultManager] fileExistsAtPath:@(path.c_str())]) {
488 return {};
489 }
490 NSData* data = [[NSFileManager defaultManager] contentsAtPath:@(path.c_str())];
491 return [Utils vectorOfUInt8FromData:data];
492}
493
494std::string
495utf8_make_valid(const std::string& name)
496{
497 ssize_t remaining_bytes = name.size();
498 ssize_t valid_bytes;
499 const char* remainder = name.c_str();
500 const char* invalid;
501 char* str = NULL;
502 char* pos = nullptr;
503
504 while (remaining_bytes != 0) {
505 if (utf8_validate_c_str(remainder, remaining_bytes, &invalid))
506 break;
507
508 valid_bytes = invalid - remainder;
509
510 if (str == NULL)
511 // If every byte is replaced by U+FFFD, max(strlen(string)) == 3 * name.size()
512 str = new char[3 * remaining_bytes];
513
514 pos = str;
515
516 strncpy(pos, remainder, valid_bytes);
517 pos += valid_bytes;
518
519 /* append U+FFFD REPLACEMENT CHARACTER */
520 pos[0] = '\357';
521 pos[1] = '\277';
522 pos[2] = '\275';
523
524 pos += 3;
525
526 remaining_bytes -= valid_bytes + 1;
527 remainder = invalid + 1;
528 }
529
530 if (str == NULL)
531 return std::string(name);
532
533 strncpy(pos, remainder, remaining_bytes);
534 pos += remaining_bytes;
535
536 std::string answer(str, pos - str);
537 assert(utf8_validate_c_str(answer.c_str(), -1, NULL));
538
539 delete[] str;
540
541 return answer;
542}
543
544bool
545utf8_validate_c_str(const char* str, ssize_t max_len, const char** end)
546{
547 const char* p;
548
549 if (max_len < 0)
550 p = fast_validate(str);
551 else
552 p = fast_validate_len(str, max_len);
553
554 if (end)
555 *end = p;
556
557 if ((max_len >= 0 && p != str + max_len) || (max_len < 0 && *p != '\0'))
558 return false;
559 else
560 return true;
561}
562
563static const char*
564fast_validate(const char* str)
565{
566 char32_t val = 0;
567 char32_t min = 0;
568 const char* p;
569
570 for (p = str; *p; p++) {
571 if (*(unsigned char*) p < 128)
572 /* done */;
573 else {
574 const char* last;
575
576 last = p;
577
578 if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
579 if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
580 goto error;
581
582 p++;
583
584 if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
585 goto error;
586 } else {
587 if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
588 min = (1 << 11);
589 val = *(unsigned char*) p & 0x0f;
590 goto TWO_REMAINING;
591 } else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
592 min = (1 << 16);
593 val = *(unsigned char*) p & 0x07;
594 } else
595 goto error;
596
597 p++;
598 CONTINUATION_CHAR;
599 TWO_REMAINING:
600 p++;
601 CONTINUATION_CHAR;
602 p++;
603 CONTINUATION_CHAR;
604
605 if (UNLIKELY(val < min))
606 goto error;
607
608 if (UNLIKELY(!UNICODE_VALID(val)))
609 goto error;
610 }
611
612 continue;
613
614 error:
615 return last;
616 }
617 }
618
619 return p;
620}
621
622static const char*
623fast_validate_len(const char* str, ssize_t max_len)
624{
625 char32_t val = 0;
626 char32_t min = 0;
627 const char* p;
628
629 assert(max_len >= 0);
630
631 for (p = str; ((p - str) < max_len) && *p; p++) {
632 if (*(unsigned char*) p < 128)
633 /* done */;
634 else {
635 const char* last;
636
637 last = p;
638
639 if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
640 if (UNLIKELY(max_len - (p - str) < 2))
641 goto error;
642
643 if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
644 goto error;
645
646 p++;
647
648 if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
649 goto error;
650 } else {
651 if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
652 if (UNLIKELY(max_len - (p - str) < 3))
653 goto error;
654
655 min = (1 << 11);
656 val = *(unsigned char*) p & 0x0f;
657 goto TWO_REMAINING;
658 } else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
659 if (UNLIKELY(max_len - (p - str) < 4))
660 goto error;
661
662 min = (1 << 16);
663 val = *(unsigned char*) p & 0x07;
664 } else
665 goto error;
666
667 p++;
668 CONTINUATION_CHAR;
669 TWO_REMAINING:
670 p++;
671 CONTINUATION_CHAR;
672 p++;
673 CONTINUATION_CHAR;
674
675 if (UNLIKELY(val < min))
676 goto error;
677
678 if (UNLIKELY(!UNICODE_VALID(val)))
679 goto error;
680 }
681
682 continue;
683
684 error:
685 return last;
686 }
687 }
688
689 return p;
690}
691
692@end