blob: b26ea442199fd444a1e982e09a181604198d731d [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
Kateryna Kostiukc3c00162022-11-10 11:21:14 -050044using namespace libjami;
kkostiuk74d1ae42021-06-17 11:10:15 -040045
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
kkostiuk74d1ae42021-06-17 11:10:15 -040057// Constants
58const std::string fileSeparator = "/";
59NSString* const certificates = @"certificates";
60NSString* const crls = @"crls";
61NSString* const ocsp = @"ocsp";
kkostiuke10e6572022-07-26 15:41:12 -040062NSString* const nameCache = @"namecache";
63NSString* const defaultNameServer = @"ns.jami.net";
64std::string const nameServerConfiguration = "RingNS.uri";
65NSString* const accountConfig = @"config.yml";
kkostiuk74d1ae42021-06-17 11:10:15 -040066
67std::map<std::string, std::shared_ptr<CallbackWrapperBase>> confHandlers;
kkostiuke10e6572022-07-26 15:41:12 -040068std::map<std::string, std::string> cachedNames;
69std::map<std::string, std::string> nameServers;
kkostiuk74d1ae42021-06-17 11:10:15 -040070
71#pragma mark Callbacks registration
72- (void)registerSignals
73{
74 confHandlers.insert(exportable_callback<ConfigurationSignal::GetAppDataPath>(
Kateryna Kostiuk2ff2e8b2023-09-18 15:34:12 -040075 [](const std::string& name, std::vector<std::string>* ret) {
76 if (name == "cache") {
77 auto path = [Constants cachesPath];
78 ret->push_back(std::string([path.path UTF8String]));
79 } else {
80 auto path = [Constants documentsPath];
81 ret->push_back(std::string([path.path UTF8String]));
82 }
83 }));
kkostiuk74d1ae42021-06-17 11:10:15 -040084
85 confHandlers.insert(exportable_callback<ConversationSignal::MessageReceived>(
Kateryna Kostiuk2ff2e8b2023-09-18 15:34:12 -040086 [weakDelegate = Adapter.delegate](const std::string& accountId,
87 const std::string& conversationId,
88 std::map<std::string, std::string> message) {
89 id<AdapterDelegate> delegate = weakDelegate;
90 if (delegate) {
91 NSString* convId = [NSString stringWithUTF8String:conversationId.c_str()];
92 NSString* account = [NSString stringWithUTF8String:accountId.c_str()];
93 NSMutableDictionary* interaction = [Utils mapToDictionnary:message];
94 [delegate newInteractionWithConversationId:convId
95 accountId:account
96 message:interaction];
97 }
98 }));
kkostiuk74d1ae42021-06-17 11:10:15 -040099
100 confHandlers.insert(exportable_callback<DataTransferSignal::DataTransferEvent>(
Kateryna Kostiuk2ff2e8b2023-09-18 15:34:12 -0400101 [weakDelegate = Adapter.delegate](const std::string& account_id,
102 const std::string& conversation_id,
103 const std::string& interaction_id,
104 const std::string& file_id,
105 int eventCode) {
106 id<AdapterDelegate> delegate = weakDelegate;
107 if (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 [delegate dataTransferEventWithFileId:fileId
113 withEventCode:eventCode
114 accountId:accountId
115 conversationId:conversationId
116 interactionId:interactionId];
117 }
118 }));
kkostiuk74d1ae42021-06-17 11:10:15 -0400119
120 confHandlers.insert(exportable_callback<ConversationSignal::ConversationSyncFinished>(
Kateryna Kostiuk2ff2e8b2023-09-18 15:34:12 -0400121 [weakDelegate = Adapter.delegate](const std::string& account_id) {
122 id<AdapterDelegate> delegate = weakDelegate;
123 if (delegate) {
124 NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
125 [delegate conversationSyncCompletedWithAccountId:accountId];
126 }
127 }));
kkostiuk74d1ae42021-06-17 11:10:15 -0400128
Kateryna Kostiukfd95c422023-05-10 16:39:12 -0400129 confHandlers.insert(exportable_callback<ConversationSignal::ConversationCloned>(
Kateryna Kostiuk2ff2e8b2023-09-18 15:34:12 -0400130 [weakDelegate = Adapter.delegate](const std::string& account_id) {
131 id<AdapterDelegate> delegate = weakDelegate;
132 if (delegate) {
133 NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
134 [delegate conversationClonedWithAccountId:accountId];
135 }
136 }));
Kateryna Kostiukfd95c422023-05-10 16:39:12 -0400137
Kateryna Kostiuk84834422023-04-11 10:53:56 -0400138 confHandlers.insert(exportable_callback<ConversationSignal::ConversationRequestReceived>([weakDelegate = Adapter.delegate](const std::string& accountId, const std::string& conversationId, std::map<std::string, std::string> metadata) {
139 id<AdapterDelegate> delegate = weakDelegate;
140 if (delegate) {
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400141 NSString* accountIdStr = [NSString stringWithUTF8String:accountId.c_str()];
142 NSString* convIdStr = [NSString stringWithUTF8String:conversationId.c_str()];
143 NSMutableDictionary* info = [Utils mapToDictionnary: metadata];
Kateryna Kostiuk84834422023-04-11 10:53:56 -0400144 [delegate receivedConversationRequestWithAccountId: accountIdStr conversationId: convIdStr metadata:info];
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400145 }
146 }));
kkostiuk74d1ae42021-06-17 11:10:15 -0400147 registerSignalHandlers(confHandlers);
148}
149
150#pragma mark AdapterDelegate
151+ (id<AdapterDelegate>)delegate
152{
153 return _delegate;
154}
155
156+ (void)setDelegate:(id<AdapterDelegate>)delegate
157{
158 _delegate = delegate;
159}
160
161- (bool)downloadFileWithFileId:(NSString*)fileId
162 accountId:(NSString*)accountId
163 conversationId:(NSString*)conversationId
164 interactionId:(NSString*)interactionId
165 withFilePath:(NSString*)filePath
166{
167 return downloadFile(std::string([accountId UTF8String]),
168 std::string([conversationId UTF8String]),
169 std::string([interactionId UTF8String]),
170 std::string([fileId UTF8String]),
171 std::string([filePath UTF8String]));
172}
173
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400174- (BOOL)start:(NSString*)accountId
kkostiuk74d1ae42021-06-17 11:10:15 -0400175{
176 [self registerSignals];
Kateryna Kostiukc3c00162022-11-10 11:21:14 -0500177 if (initialized() == true) {
Kateryna Kostiukdfee1c92023-03-29 10:22:52 -0400178 reloadConversationsAndRequests(std::string([accountId UTF8String]));
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400179 setAccountActive(std::string([accountId UTF8String]), true);
kkostiuk74d1ae42021-06-17 11:10:15 -0400180 return true;
181 }
182#if DEBUG
Kateryna Kostiukd500b5d2023-05-17 16:20:00 -0400183 int flag = LIBJAMI_FLAG_CONSOLE_LOG | LIBJAMI_FLAG_DEBUG | LIBJAMI_FLAG_IOS_EXTENSION | LIBJAMI_FLAG_NO_AUTOSYNC | LIBJAMI_FLAG_NO_LOCAL_AUDIO;
kkostiuk74d1ae42021-06-17 11:10:15 -0400184#else
Kateryna Kostiukd500b5d2023-05-17 16:20:00 -0400185 int flag = LIBJAMI_FLAG_IOS_EXTENSION | LIBJAMI_FLAG_NO_AUTOSYNC | LIBJAMI_FLAG_NO_LOCAL_AUDIO;
kkostiuk74d1ae42021-06-17 11:10:15 -0400186#endif
187 if (![[NSThread currentThread] isMainThread]) {
188 __block bool success;
189 dispatch_sync(dispatch_get_main_queue(), ^{
Kateryna Kostiukc3c00162022-11-10 11:21:14 -0500190 if (init(static_cast<InitFlag>(flag))) {
kkostiuk74d1ae42021-06-17 11:10:15 -0400191 success = start({});
192 } else {
193 success = false;
194 }
195 });
196 return success;
197 } else {
Kateryna Kostiukc3c00162022-11-10 11:21:14 -0500198 if (init(static_cast<InitFlag>(flag))) {
kkostiuk74d1ae42021-06-17 11:10:15 -0400199 return start({});
200 }
201 return false;
202 }
203}
204
205- (void)stop
206{
207 unregisterSignalHandlers();
208 confHandlers.clear();
209 [self setAccountsActive:false];
210}
211
212- (void)setAccountsActive:(BOOL)active
213{
214 auto accounts = getAccountList();
215 for (auto account : accounts) {
Kateryna Kostiukdfee1c92023-03-29 10:22:52 -0400216 if (active) {
217 reloadConversationsAndRequests(account);
218 }
kkostiuk39672932022-07-12 11:36:30 -0400219 setAccountActive(account, active, true);
kkostiuk74d1ae42021-06-17 11:10:15 -0400220 }
221}
222
Kateryna Kostiuk2ff2e8b2023-09-18 15:34:12 -0400223- (void)pushNotificationReceived:(NSString*)from message:(NSDictionary*)data {
224 pushNotificationReceived(std::string([from UTF8String]), [Utils dictionnaryToMap:data]);
225}
226
kkostiuk74d1ae42021-06-17 11:10:15 -0400227- (NSDictionary<NSString*, NSString*>*)decrypt:(NSString*)keyPath
Kateryna Kostiukba9a7b12023-05-02 09:33:52 -0400228 accountId:(NSString*)accountId
kkostiuk74d1ae42021-06-17 11:10:15 -0400229 treated:(NSString*)treatedMessagesPath
230 value:(NSDictionary*)value
231{
232 if (![[NSFileManager defaultManager] fileExistsAtPath:keyPath]) {
233 return {};
234 }
235
236 NSData* data = [[NSFileManager defaultManager] contentsAtPath:keyPath];
237 const uint8_t* bytes = (const uint8_t*) [data bytes];
238 dht::crypto::PrivateKey dhtKey(bytes, [data length], "");
239
240 Json::Value jsonValue = toJson(value);
241 dht::Value dhtValue(jsonValue);
242
243 if (!dhtValue.isEncrypted()) {
244 return {};
245 }
246 try {
kkostiuk03fa94c2022-07-14 16:30:35 -0400247 dht::Sp<dht::Value> decrypted = dhtValue.decrypt(dhtKey);
248 auto unpacked = msgpack::unpack((const char*) decrypted->data.data(), decrypted->data.size());
kkostiuk74d1ae42021-06-17 11:10:15 -0400249 auto peerCR = unpacked.get().as<PeerConnectionRequest>();
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400250 if (peerCR.connType.empty()) {
251 // this value is not a PeerConnectionRequest
252 // check if it a TrustRequest
253 auto conversationRequest = unpacked.get().as<dht::TrustRequest>();
Kateryna Kostiukfd95c422023-05-10 16:39:12 -0400254 if (conversationRequest.confirm) {
255 // request confirmation. We need to wait for conversation to clone
256 return @{@"": @"application/clone"};
257 }
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400258 if (!conversationRequest.conversationId.empty()) {
259 if (conversationRequest.service == "cx.ring") {
260 // return git message type to start daemon
261 return @{@"": @"application/im-gitmessage-id"};
262 }
263 }
264 return {};
265 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400266 if (isMessageTreated(peerCR.id, [treatedMessagesPath UTF8String])) {
267 return {};
268 }
Kateryna Kostiukf099b5c2023-05-02 09:11:49 -0400269
270 std::string peerId = "";
271 if (peerCR.connType == "videoCall" || peerCR.connType == "audioCall") {
Kateryna Kostiukba9a7b12023-05-02 09:33:52 -0400272 auto certPath = [[[Constants documentsPath] URLByAppendingPathComponent:accountId] URLByAppendingPathComponent:certificates].path.UTF8String;
273 auto crlPath = [[[Constants documentsPath] URLByAppendingPathComponent:accountId] URLByAppendingPathComponent:crls].path.UTF8String;
274 auto ocspPath = [[[Constants documentsPath] URLByAppendingPathComponent:accountId] URLByAppendingPathComponent:ocsp].path.UTF8String;
Kateryna Kostiukf099b5c2023-05-02 09:11:49 -0400275 peerId = getPeerId(decrypted->owner->getId().toString(),
276 certPath,
277 crlPath,
278 ocspPath);
279 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400280 return @{@(peerId.c_str()): @(peerCR.connType.c_str())};
281 } catch (std::runtime_error error) {
282 }
283 return {};
284}
285
kkostiuke10e6572022-07-26 15:41:12 -0400286-(NSString*)getNameFor:(NSString*)address accountId:(NSString*)accountId {
287 return @(getName(std::string([address UTF8String]), std::string([accountId UTF8String])).c_str());
288}
289
290-(NSString*)nameServerForAccountId:(NSString*)accountId; {
291 auto nameServer = getNameServer(std::string([accountId UTF8String]));
292 return nameServer.empty() ? defaultNameServer : @(nameServer.c_str());
293}
294
kkostiuk74d1ae42021-06-17 11:10:15 -0400295Json::Value
296toJson(NSDictionary* value)
297{
298 Json::Value val;
299 for (NSString* key in value.allKeys) {
300 if ([[value objectForKey:key] isKindOfClass:[NSString class]]) {
301 NSString* stringValue = [value objectForKey:key];
302 val[key.UTF8String] = stringValue.UTF8String;
303 } else if ([[value objectForKey:key] isKindOfClass:[NSNumber class]]) {
304 NSNumber* number = [value objectForKey:key];
305 if ([key isEqualToString:@"id"]) {
306 unsigned long long int intValue = [number unsignedLongLongValue];
307 val[key.UTF8String] = intValue;
308 } else {
309 int intValue = [number intValue];
310 val[key.UTF8String] = intValue;
311 }
312 }
313 }
314 return val;
315}
316
kkostiuke10e6572022-07-26 15:41:12 -0400317std::string getName(std::string addres, std::string accountId)
318{
319 auto name = cachedNames.find(addres);
320 if (name != cachedNames.end()) {
321 return name->second;
322 }
323
324 auto ns = getNameServer(accountId);
Kateryna Kostiuk1c0e7562022-08-23 15:17:44 -0400325 NSURL *url = [NSURL URLWithString: @(ns.c_str())];
326 NSString* host = [url host];
327 NSString* nameServer = host.length == 0 ? defaultNameServer : host;
kkostiuke10e6572022-07-26 15:41:12 -0400328 std::string namesPath = [[[Constants cachesPath] URLByAppendingPathComponent: nameCache] URLByAppendingPathComponent: nameServer].path.UTF8String;
329
330 msgpack::unpacker pac;
331 // read file
332 std::ifstream file = std::ifstream(namesPath, std::ios_base::in);
333 if (!file.is_open()) {
334 return "";
335 }
336 std::string line;
337 while (std::getline(file, line)) {
338 pac.reserve_buffer(line.size());
339 memcpy(pac.buffer(), line.data(), line.size());
340 pac.buffer_consumed(line.size());
341 }
342
343 // load values
344 msgpack::object_handle oh;
345 if (pac.next(oh))
346 oh.get().convert(cachedNames);
347 auto cacheRes = cachedNames.find(addres);
348 return cacheRes != cachedNames.end() ? cacheRes->second : std::string {};
349}
350
351std::string getNameServer(std::string accountId) {
352 auto it = nameServers.find(accountId);
353 if (it != nameServers.end()) {
354 return it->second;
355 }
356 std::string nameServer {};
357 auto accountConfigPath = [[[Constants documentsPath] URLByAppendingPathComponent: @(accountId.c_str())] URLByAppendingPathComponent: accountConfig].path.UTF8String;
358 try {
359 std::ifstream file = std::ifstream(accountConfigPath, std::ios_base::in);
360 YAML::Node node = YAML::Load(file);
361 file.close();
362 nameServer = node[nameServerConfiguration].as<std::string>();
363 if (!nameServer.empty()) {
364 nameServers.insert(std::pair<std::string, std::string>(accountId, nameServer));
365 }
366 } catch (const std::exception& e) {}
367 return nameServer;
368}
369
kkostiuk74d1ae42021-06-17 11:10:15 -0400370#pragma mark functions copied from the daemon
371
372#define LIKELY(expr) (expr)
373#define UNLIKELY(expr) (expr)
374
375/*
376 * Check whether a Unicode (5.2) char is in a valid range.
377 *
378 * The first check comes from the Unicode guarantee to never encode
379 * a point above 0x0010ffff, since UTF-16 couldn't represent it.
380 *
381 * The second check covers surrogate pairs (category Cs).
382 *
383 * @param Char the character
384 */
385#define UNICODE_VALID(Char) ((Char) < 0x110000 && (((Char) &0xFFFFF800) != 0xD800))
386
387#define CONTINUATION_CHAR \
388 if ((*(unsigned char*) p & 0xc0) != 0x80) /* 10xxxxxx */ \
389 goto error; \
390 val <<= 6; \
391 val |= (*(unsigned char*) p) & 0x3f;
392
kkostiuk74d1ae42021-06-17 11:10:15 -0400393template<typename ID = dht::Value::Id>
394bool
395isMessageTreated(ID messageId, const std::string& path)
396{
397 std::ifstream file = std::ifstream(path, std::ios_base::in);
398 if (!file.is_open()) {
399 return false;
400 }
401 std::set<ID, std::less<>> treatedMessages;
402 std::string line;
403 while (std::getline(file, line)) {
404 if constexpr (std::is_same<ID, std::string>::value) {
405 treatedMessages.emplace(std::move(line));
406 } else if constexpr (std::is_integral<ID>::value) {
407 ID vid;
408 if (auto [p, ec] = std::from_chars(line.data(), line.data() + line.size(), vid, 16);
409 ec == std::errc()) {
410 treatedMessages.emplace(vid);
411 }
412 }
413 }
414 return treatedMessages.find(messageId) != treatedMessages.end();
415}
416
417std::string
418getPeerId(const std::string& key,
419 const std::string& certPath,
420 const std::string& crlPath,
421 const std::string& ocspPath)
422{
423 std::map<std::string, std::shared_ptr<dht::crypto::Certificate>> certs;
424 auto dir_content = readDirectory(certPath);
425 unsigned n = 0;
426 for (const auto& f : dir_content) {
427 try {
428 auto crt = std::make_shared<dht::crypto::Certificate>(
429 loadFile(certPath + fileSeparator + f));
430 auto id = crt->getId().toString();
431 auto longId = crt->getLongId().toString();
432 if (id != f && longId != f)
433 throw std::logic_error("Certificate id mismatch");
434 while (crt) {
435 id = crt->getId().toString();
436 longId = crt->getLongId().toString();
437 certs.emplace(std::move(id), crt);
438 certs.emplace(std::move(longId), crt);
439 loadRevocations(*crt, crlPath, ocspPath);
440 crt = crt->issuer;
441 ++n;
442 }
443 } catch (const std::exception& e) {
444 }
445 }
446 auto cit = certs.find(key);
447 if (cit == certs.cend()) {
448 return {};
449 }
450 dht::InfoHash peer_account_id;
451 if (not foundPeerDevice(cit->second, peer_account_id)) {
452 return {};
453 }
454 return peer_account_id.toString();
455}
456
457void
458loadRevocations(dht::crypto::Certificate& crt,
459 const std::string& crlPath,
460 const std::string& ocspPath)
461{
462 auto dir = crlPath + fileSeparator + crt.getId().toString();
463 for (const auto& crl : readDirectory(dir)) {
464 try {
465 crt.addRevocationList(
466 std::make_shared<dht::crypto::RevocationList>(loadFile(dir + fileSeparator + crl)));
467 } catch (const std::exception& e) {
468 }
469 }
470 auto ocsp_dir = ocspPath + fileSeparator + crt.getId().toString();
471 for (const auto& ocsp : readDirectory(ocsp_dir)) {
472 try {
473 std::string ocsp_filepath = ocsp_dir + fileSeparator + ocsp;
474 auto serial = crt.getSerialNumber();
475 if (dht::toHex(serial.data(), serial.size()) != ocsp)
476 continue;
477 dht::Blob ocspBlob = loadFile(ocsp_filepath);
478 crt.ocspResponse = std::make_shared<dht::crypto::OcspResponse>(ocspBlob.data(),
479 ocspBlob.size());
480 } catch (const std::exception& e) {
481 }
482 }
483}
484
485bool
486foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& crt, dht::InfoHash& account_id)
487{
488 if (not crt)
489 return false;
490
491 auto top_issuer = crt;
492 while (top_issuer->issuer)
493 top_issuer = top_issuer->issuer;
494
495 if (top_issuer == crt) {
496 return false;
497 }
498 dht::crypto::TrustList peer_trust;
499 peer_trust.add(*top_issuer);
500 if (not peer_trust.verify(*crt)) {
501 return false;
502 }
503 if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD) {
504 return false;
505 }
506 account_id = crt->issuer->getId();
507 return true;
508}
509
510std::vector<std::string>
511readDirectory(const std::string& dir)
512{
513 NSError* error;
514 NSFileManager* fileMgr = [NSFileManager defaultManager];
515 NSArray* files = [fileMgr contentsOfDirectoryAtPath:@(dir.c_str()) error:&error];
516
517 std::vector<std::string> vector;
518 for (NSString* fileName in files) {
519 vector.push_back([fileName UTF8String]);
520 }
521 return vector;
522}
523
524std::vector<uint8_t>
525loadFile(const std::string& path)
526{
527 if (![[NSFileManager defaultManager] fileExistsAtPath:@(path.c_str())]) {
528 return {};
529 }
530 NSData* data = [[NSFileManager defaultManager] contentsAtPath:@(path.c_str())];
531 return [Utils vectorOfUInt8FromData:data];
532}
533
534std::string
535utf8_make_valid(const std::string& name)
536{
537 ssize_t remaining_bytes = name.size();
538 ssize_t valid_bytes;
539 const char* remainder = name.c_str();
540 const char* invalid;
541 char* str = NULL;
542 char* pos = nullptr;
543
544 while (remaining_bytes != 0) {
545 if (utf8_validate_c_str(remainder, remaining_bytes, &invalid))
546 break;
547
548 valid_bytes = invalid - remainder;
549
550 if (str == NULL)
551 // If every byte is replaced by U+FFFD, max(strlen(string)) == 3 * name.size()
552 str = new char[3 * remaining_bytes];
553
554 pos = str;
555
556 strncpy(pos, remainder, valid_bytes);
557 pos += valid_bytes;
558
559 /* append U+FFFD REPLACEMENT CHARACTER */
560 pos[0] = '\357';
561 pos[1] = '\277';
562 pos[2] = '\275';
563
564 pos += 3;
565
566 remaining_bytes -= valid_bytes + 1;
567 remainder = invalid + 1;
568 }
569
570 if (str == NULL)
571 return std::string(name);
572
573 strncpy(pos, remainder, remaining_bytes);
574 pos += remaining_bytes;
575
576 std::string answer(str, pos - str);
577 assert(utf8_validate_c_str(answer.c_str(), -1, NULL));
578
579 delete[] str;
580
581 return answer;
582}
583
584bool
585utf8_validate_c_str(const char* str, ssize_t max_len, const char** end)
586{
587 const char* p;
588
589 if (max_len < 0)
590 p = fast_validate(str);
591 else
592 p = fast_validate_len(str, max_len);
593
594 if (end)
595 *end = p;
596
597 if ((max_len >= 0 && p != str + max_len) || (max_len < 0 && *p != '\0'))
598 return false;
599 else
600 return true;
601}
602
603static const char*
604fast_validate(const char* str)
605{
606 char32_t val = 0;
607 char32_t min = 0;
608 const char* p;
609
610 for (p = str; *p; p++) {
611 if (*(unsigned char*) p < 128)
612 /* done */;
613 else {
614 const char* last;
615
616 last = p;
617
618 if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
619 if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
620 goto error;
621
622 p++;
623
624 if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
625 goto error;
626 } else {
627 if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
628 min = (1 << 11);
629 val = *(unsigned char*) p & 0x0f;
630 goto TWO_REMAINING;
631 } else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
632 min = (1 << 16);
633 val = *(unsigned char*) p & 0x07;
634 } else
635 goto error;
636
637 p++;
638 CONTINUATION_CHAR;
639 TWO_REMAINING:
640 p++;
641 CONTINUATION_CHAR;
642 p++;
643 CONTINUATION_CHAR;
644
645 if (UNLIKELY(val < min))
646 goto error;
647
648 if (UNLIKELY(!UNICODE_VALID(val)))
649 goto error;
650 }
651
652 continue;
653
654 error:
655 return last;
656 }
657 }
658
659 return p;
660}
661
662static const char*
663fast_validate_len(const char* str, ssize_t max_len)
664{
665 char32_t val = 0;
666 char32_t min = 0;
667 const char* p;
668
669 assert(max_len >= 0);
670
671 for (p = str; ((p - str) < max_len) && *p; p++) {
672 if (*(unsigned char*) p < 128)
673 /* done */;
674 else {
675 const char* last;
676
677 last = p;
678
679 if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
680 if (UNLIKELY(max_len - (p - str) < 2))
681 goto error;
682
683 if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
684 goto error;
685
686 p++;
687
688 if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
689 goto error;
690 } else {
691 if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
692 if (UNLIKELY(max_len - (p - str) < 3))
693 goto error;
694
695 min = (1 << 11);
696 val = *(unsigned char*) p & 0x0f;
697 goto TWO_REMAINING;
698 } else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
699 if (UNLIKELY(max_len - (p - str) < 4))
700 goto error;
701
702 min = (1 << 16);
703 val = *(unsigned char*) p & 0x07;
704 } else
705 goto error;
706
707 p++;
708 CONTINUATION_CHAR;
709 TWO_REMAINING:
710 p++;
711 CONTINUATION_CHAR;
712 p++;
713 CONTINUATION_CHAR;
714
715 if (UNLIKELY(val < min))
716 goto error;
717
718 if (UNLIKELY(!UNICODE_VALID(val)))
719 goto error;
720 }
721
722 continue;
723
724 error:
725 return last;
726 }
727 }
728
729 return p;
730}
731
732@end