whalibmob 5.14.7 → 5.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Client.js +138 -10
- package/lib/Registration.js +161 -16
- package/lib/image/Bmp.js +16 -4
- package/lib/signal/SignalStore.js +152 -29
- package/package.json +1 -1
package/lib/Client.js
CHANGED
|
@@ -442,9 +442,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
442
442
|
async init(phoneNumber) {
|
|
443
443
|
phoneNumber = String(phoneNumber).replace(/\D/g, '');
|
|
444
444
|
this._phoneNumber = phoneNumber;
|
|
445
|
-
// A fresh attempt clears whatever made the last one fatal
|
|
446
|
-
//
|
|
447
|
-
|
|
445
|
+
// A fresh attempt clears whatever made the last one fatal, and whatever
|
|
446
|
+
// deliberate disconnect came before it. Only the public entry points reset
|
|
447
|
+
// these — the reconnect loop must not clear its own guards.
|
|
448
|
+
this._fatal = false;
|
|
449
|
+
this._closing = false;
|
|
448
450
|
|
|
449
451
|
this._sessionDir = sessionDirFor(this._baseDir, phoneNumber);
|
|
450
452
|
|
|
@@ -621,7 +623,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
621
623
|
phoneNumber = String(phoneNumber).replace(/\D/g, '');
|
|
622
624
|
this._phoneNumber = phoneNumber;
|
|
623
625
|
this._mode = 'web';
|
|
624
|
-
|
|
626
|
+
// Same as init(): a fresh attempt lifts both guards from the last one.
|
|
627
|
+
this._fatal = false;
|
|
628
|
+
this._closing = false;
|
|
625
629
|
|
|
626
630
|
const { createNewWebStore, loadWebStore, saveWebStore, webSessionPath } =
|
|
627
631
|
require('./WebStore');
|
|
@@ -1066,13 +1070,34 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1066
1070
|
return idx > 0 ? `${lidUser}:${idx}@lid` : `${lidUser}@lid`;
|
|
1067
1071
|
}
|
|
1068
1072
|
|
|
1069
|
-
|
|
1073
|
+
/**
|
|
1074
|
+
* Close the connection, and keep it closed.
|
|
1075
|
+
*
|
|
1076
|
+
* Connect again through init() / initWeb() — those are where the caller says
|
|
1077
|
+
* it wants a connection, and where the guard below is lifted.
|
|
1078
|
+
*
|
|
1079
|
+
* @param {object} [opts]
|
|
1080
|
+
* @param {boolean} [opts.reconnect=false] drop the current socket but leave
|
|
1081
|
+
* the reconnect loop armed. For a failure the server has told us is
|
|
1082
|
+
* temporary, where coming back is the right answer.
|
|
1083
|
+
*/
|
|
1084
|
+
disconnect(opts) {
|
|
1070
1085
|
// Closing the socket makes it emit 'close', which lands in _onClose and
|
|
1071
1086
|
// schedules a reconnect. That is right for a dropped connection and wrong
|
|
1072
1087
|
// for a deliberate teardown — and it is what turned a single <failure> into
|
|
1073
1088
|
// an endless retry loop, since clearing _reconnecting was not enough to
|
|
1074
1089
|
// stop the handler that sets it again.
|
|
1075
|
-
|
|
1090
|
+
//
|
|
1091
|
+
// For that to work the guard has to still be set when the 'close' arrives,
|
|
1092
|
+
// and it arrives on a later tick — a socket is destroyed now and reports it
|
|
1093
|
+
// afterwards. Clearing it at the end of this method, which is what it used
|
|
1094
|
+
// to do, meant _scheduleReconnect() never once saw it set: every deliberate
|
|
1095
|
+
// disconnect dialled straight back in a second later, and /reconnect ended
|
|
1096
|
+
// up with the old session live alongside the new one.
|
|
1097
|
+
//
|
|
1098
|
+
// Only ever set here, never cleared, so a teardown that means it cannot be
|
|
1099
|
+
// undone by a later one that does not.
|
|
1100
|
+
if (!(opts && opts.reconnect)) this._closing = true;
|
|
1076
1101
|
this._reconnecting = false;
|
|
1077
1102
|
this._reconnectTry = 0;
|
|
1078
1103
|
this._stopTimers();
|
|
@@ -1082,7 +1107,6 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1082
1107
|
}
|
|
1083
1108
|
this._connected = false;
|
|
1084
1109
|
this.emit('close');
|
|
1085
|
-
this._closing = false;
|
|
1086
1110
|
}
|
|
1087
1111
|
|
|
1088
1112
|
/**
|
|
@@ -1246,8 +1270,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1246
1270
|
|
|
1247
1271
|
this._requestMediaConnection();
|
|
1248
1272
|
// Ask what the server still has rather than uploading blind. It answers
|
|
1249
|
-
// with a number this side has no other way of learning
|
|
1250
|
-
|
|
1273
|
+
// with a number this side has no other way of learning, and then with the
|
|
1274
|
+
// bundle it is serving, which is the thing that goes quietly wrong and
|
|
1275
|
+
// takes the account's reachability with it.
|
|
1276
|
+
this._ensureServerPreKeys('login')
|
|
1277
|
+
.then(() => this._verifyServerKeyBundle('login'));
|
|
1251
1278
|
|
|
1252
1279
|
// Same telemetry ping the web client sends on every completed login.
|
|
1253
1280
|
this._sendUnifiedSession();
|
|
@@ -3277,7 +3304,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3277
3304
|
this.emit('auth_failure', { reason, location: loc, loggedOut: spent, node });
|
|
3278
3305
|
this._fatal = spent;
|
|
3279
3306
|
this._reconnecting = false;
|
|
3280
|
-
this
|
|
3307
|
+
// A reason that did not kill the session is one to come back from, so this
|
|
3308
|
+
// teardown leaves the reconnect loop armed — a 503 is the server having a
|
|
3309
|
+
// bad minute, not a reason to abandon a working registration. A reason that
|
|
3310
|
+
// did kill it is fatal, and _scheduleReconnect() stands down on that itself.
|
|
3311
|
+
this.disconnect({ reconnect: !spent });
|
|
3281
3312
|
}
|
|
3282
3313
|
|
|
3283
3314
|
// ─── Media connection ─────────────────────────────────────────────────────
|
|
@@ -3411,6 +3442,103 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3411
3442
|
return Array.isArray(list.content) ? list.content.length : 0;
|
|
3412
3443
|
}
|
|
3413
3444
|
|
|
3445
|
+
// What the server believes this account's key bundle is.
|
|
3446
|
+
//
|
|
3447
|
+
// The digest is the only way to see it. Everything else here is a guess about
|
|
3448
|
+
// what was uploaded once and assumed to have stuck — and when the server's
|
|
3449
|
+
// idea of the bundle and ours part company, nobody can open a session with
|
|
3450
|
+
// this account and there is no error anywhere to say so. It is the state that
|
|
3451
|
+
// registering the number again clears, which is why registering again is what
|
|
3452
|
+
// has been fixing it.
|
|
3453
|
+
async _queryKeyDigest() {
|
|
3454
|
+
if (!this._socket || !this._connected) return null;
|
|
3455
|
+
|
|
3456
|
+
const resp = await this._sendIq(new BinaryNode('iq', {
|
|
3457
|
+
id: this._genMsgId(),
|
|
3458
|
+
to: 's.whatsapp.net',
|
|
3459
|
+
type: 'get',
|
|
3460
|
+
xmlns: 'encrypt'
|
|
3461
|
+
}, [new BinaryNode('digest', {}, null)]));
|
|
3462
|
+
|
|
3463
|
+
if (!resp) return null;
|
|
3464
|
+
if (resp.attrs && resp.attrs.type === 'error') return null;
|
|
3465
|
+
|
|
3466
|
+
const digest = findChild(resp, 'digest');
|
|
3467
|
+
if (!digest) return { missing: true };
|
|
3468
|
+
|
|
3469
|
+
const bytesOf = (parent, tag) => {
|
|
3470
|
+
const n = parent && findChild(parent, tag);
|
|
3471
|
+
if (!n) return null;
|
|
3472
|
+
if (Buffer.isBuffer(n.content)) return n.content;
|
|
3473
|
+
return n.content ? Buffer.from(n.content) : null;
|
|
3474
|
+
};
|
|
3475
|
+
|
|
3476
|
+
const skey = findChild(digest, 'skey');
|
|
3477
|
+
const list = findChild(digest, 'list');
|
|
3478
|
+
return {
|
|
3479
|
+
missing: false,
|
|
3480
|
+
identity: bytesOf(digest, 'identity'),
|
|
3481
|
+
skeyId: bytesOf(skey, 'id'),
|
|
3482
|
+
skeyValue: bytesOf(skey, 'value'),
|
|
3483
|
+
preKeyCount: list && Array.isArray(list.content) ? list.content.length : 0
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// Compare that bundle against the one we hold, and re-upload when they differ.
|
|
3488
|
+
//
|
|
3489
|
+
// Only values the server states outright are compared — the identity key and
|
|
3490
|
+
// the signed pre-key it is serving — so nothing here depends on guessing how
|
|
3491
|
+
// it hashes them. A mismatch means the bundle other people are handed is not
|
|
3492
|
+
// the one this session can answer for, and no amount of waiting fixes that.
|
|
3493
|
+
async _verifyServerKeyBundle(reason) {
|
|
3494
|
+
if (!this._signal) return;
|
|
3495
|
+
try {
|
|
3496
|
+
const digest = await this._queryKeyDigest();
|
|
3497
|
+
if (!digest) return; // no answer; nothing to conclude
|
|
3498
|
+
|
|
3499
|
+
if (digest.missing) {
|
|
3500
|
+
_whaDbg('[DBG] KEYBUNDLE server returned no digest (' + reason + ') — re-uploading');
|
|
3501
|
+
this._replenishLocalPreKeys();
|
|
3502
|
+
this._uploadPreKeys();
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
const bare = (b) => (b && b.length === 33 && b[0] === 0x05) ? b.slice(1) : b;
|
|
3507
|
+
const ourIdent = bare(this._signal.getIdentityKey());
|
|
3508
|
+
const ourSpk = this._signal.getSignedPreKeyForUpload();
|
|
3509
|
+
const ourSpkPub = ourSpk && bare(ourSpk.keyPair.pubKey);
|
|
3510
|
+
|
|
3511
|
+
const mismatches = [];
|
|
3512
|
+
if (digest.identity && ourIdent &&
|
|
3513
|
+
Buffer.compare(digest.identity, ourIdent) !== 0) {
|
|
3514
|
+
mismatches.push('identity key');
|
|
3515
|
+
}
|
|
3516
|
+
if (digest.skeyValue && ourSpkPub &&
|
|
3517
|
+
Buffer.compare(digest.skeyValue, ourSpkPub) !== 0) {
|
|
3518
|
+
mismatches.push('signed pre-key');
|
|
3519
|
+
}
|
|
3520
|
+
if (digest.skeyId && ourSpk) {
|
|
3521
|
+
let served = 0;
|
|
3522
|
+
for (const b of digest.skeyId) served = (served << 8) | b;
|
|
3523
|
+
if (served !== ourSpk.keyId) mismatches.push('signed pre-key id (' + served +
|
|
3524
|
+
' vs ' + ourSpk.keyId + ')');
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
if (!mismatches.length) {
|
|
3528
|
+
_whaDbg('[DBG] KEYBUNDLE matches the server (' + reason + '), ' +
|
|
3529
|
+
digest.preKeyCount + ' one-time key(s) there');
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
_whaDbg('[DBG] KEYBUNDLE server disagrees on ' + mismatches.join(' and ') +
|
|
3534
|
+
' (' + reason + ') — re-uploading');
|
|
3535
|
+
this._replenishLocalPreKeys();
|
|
3536
|
+
this._uploadPreKeys();
|
|
3537
|
+
} catch (err) {
|
|
3538
|
+
_whaDbg('[DBG] KEYBUNDLE check failed (' + reason + '): ' + (err && err.message));
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3414
3542
|
// Top the server back up when it is running out. Never throws: a session that
|
|
3415
3543
|
// cannot ask is left exactly as it was rather than losing its connection over
|
|
3416
3544
|
// a bookkeeping query.
|
package/lib/Registration.js
CHANGED
|
@@ -418,6 +418,149 @@ const COUNTRY_META = {
|
|
|
418
418
|
'971': { mcc: '424', mnc: '02', lg: 'ar', lc: 'AE' }, // UAE
|
|
419
419
|
'972': { mcc: '425', mnc: '01', lg: 'he', lc: 'IL' }, // Israel
|
|
420
420
|
'880': { mcc: '470', mnc: '01', lg: 'bn', lc: 'BD' }, // Bangladesh
|
|
421
|
+
// Filled in from the ITU mobile-network tables so a number from anywhere
|
|
422
|
+
// reports the carrier its country actually has. Without an entry a session
|
|
423
|
+
// falls back to 000/000, which is what a handset with no SIM card reports.
|
|
424
|
+
// One operational national operator per country; the point is a plausible
|
|
425
|
+
// home network, not a particular one.
|
|
426
|
+
'53': { mcc: '368', mnc: '01', lg: 'es', lc: 'CU' }, // Cuba (CUBACEL)
|
|
427
|
+
'211': { mcc: '659', mnc: '02', lg: 'en', lc: 'SS' }, // South Sudan (MTN)
|
|
428
|
+
'220': { mcc: '607', mnc: '01', lg: 'en', lc: 'GM' }, // Gambia (Gamcel)
|
|
429
|
+
'221': { mcc: '608', mnc: '01', lg: 'fr', lc: 'SN' }, // Senegal (Orange)
|
|
430
|
+
'222': { mcc: '609', mnc: '01', lg: 'ar', lc: 'MR' }, // Mauritania (Mattel)
|
|
431
|
+
'223': { mcc: '610', mnc: '01', lg: 'fr', lc: 'ML' }, // Mali (Malitel)
|
|
432
|
+
'224': { mcc: '611', mnc: '01', lg: 'fr', lc: 'GN' }, // Guinea (Orange)
|
|
433
|
+
'225': { mcc: '612', mnc: '02', lg: 'fr', lc: 'CI' }, // Ivory Coast (Moov)
|
|
434
|
+
'226': { mcc: '613', mnc: '01', lg: 'fr', lc: 'BF' }, // Burkina Faso (Telmob)
|
|
435
|
+
'227': { mcc: '614', mnc: '01', lg: 'fr', lc: 'NE' }, // Niger (SahelCom)
|
|
436
|
+
'228': { mcc: '615', mnc: '01', lg: 'fr', lc: 'TG' }, // Togo (Togo Cell)
|
|
437
|
+
'229': { mcc: '616', mnc: '01', lg: 'fr', lc: 'BJ' }, // Benin (Benin Telecoms Mob)
|
|
438
|
+
'230': { mcc: '617', mnc: '01', lg: 'en', lc: 'MU' }, // Mauritius (my.t)
|
|
439
|
+
'231': { mcc: '618', mnc: '01', lg: 'en', lc: 'LR' }, // Liberia (Lonestar Cell MTN)
|
|
440
|
+
'232': { mcc: '619', mnc: '01', lg: 'en', lc: 'SL' }, // Sierra Leone (Orange)
|
|
441
|
+
'233': { mcc: '620', mnc: '01', lg: 'en', lc: 'GH' }, // Ghana (MTN)
|
|
442
|
+
'235': { mcc: '622', mnc: '01', lg: 'ar', lc: 'TD' }, // Chad (Airtel)
|
|
443
|
+
'236': { mcc: '623', mnc: '01', lg: 'fr', lc: 'CF' }, // Central African Republic (Moov)
|
|
444
|
+
'237': { mcc: '624', mnc: '01', lg: 'en', lc: 'CM' }, // Cameroon (MTN Cameroon)
|
|
445
|
+
'238': { mcc: '625', mnc: '01', lg: 'pt', lc: 'CV' }, // Cape Verde (CVMOVEL)
|
|
446
|
+
'239': { mcc: '626', mnc: '01', lg: 'pt', lc: 'ST' }, // São Tomé and Príncipe (CSTmovel)
|
|
447
|
+
'240': { mcc: '627', mnc: '01', lg: 'fr', lc: 'GQ' }, // Equatorial Guinea (Orange GQ)
|
|
448
|
+
'241': { mcc: '628', mnc: '01', lg: 'fr', lc: 'GA' }, // Gabon (Libertis)
|
|
449
|
+
'242': { mcc: '629', mnc: '01', lg: 'fr', lc: 'CG' }, // Republic of the Congo (Airtel)
|
|
450
|
+
'243': { mcc: '630', mnc: '01', lg: 'fr', lc: 'CD' }, // DR Congo (Vodacom)
|
|
451
|
+
'244': { mcc: '631', mnc: '02', lg: 'pt', lc: 'AO' }, // Angola (UNITEL)
|
|
452
|
+
'245': { mcc: '632', mnc: '01', lg: 'pt', lc: 'GW' }, // Guinea-Bissau (Guinetel)
|
|
453
|
+
'248': { mcc: '633', mnc: '01', lg: 'en', lc: 'SC' }, // Seychelles (Cable & Wireless)
|
|
454
|
+
'249': { mcc: '634', mnc: '01', lg: 'ar', lc: 'SD' }, // Sudan (Zain SD)
|
|
455
|
+
'250': { mcc: '635', mnc: '10', lg: 'en', lc: 'RW' }, // Rwanda (MTN)
|
|
456
|
+
'251': { mcc: '636', mnc: '01', lg: 'am', lc: 'ET' }, // Ethiopia (MTN)
|
|
457
|
+
'252': { mcc: '637', mnc: '01', lg: 'ar', lc: 'SO' }, // Somalia (Telesom)
|
|
458
|
+
'253': { mcc: '638', mnc: '01', lg: 'ar', lc: 'DJ' }, // Djibouti (Evatis)
|
|
459
|
+
'257': { mcc: '642', mnc: '01', lg: 'fr', lc: 'BI' }, // Burundi (econet Leo)
|
|
460
|
+
'258': { mcc: '643', mnc: '01', lg: 'pt', lc: 'MZ' }, // Mozambique (mCel)
|
|
461
|
+
'260': { mcc: '645', mnc: '01', lg: 'en', lc: 'ZM' }, // Zambia (Airtel)
|
|
462
|
+
'261': { mcc: '646', mnc: '01', lg: 'fr', lc: 'MG' }, // Madagascar (Airtel)
|
|
463
|
+
'263': { mcc: '648', mnc: '01', lg: 'en', lc: 'ZW' }, // Zimbabwe (Net*One)
|
|
464
|
+
'264': { mcc: '649', mnc: '01', lg: 'af', lc: 'NA' }, // Namibia (MTC)
|
|
465
|
+
'265': { mcc: '650', mnc: '01', lg: 'en', lc: 'MW' }, // Malawi (TNM)
|
|
466
|
+
'266': { mcc: '651', mnc: '01', lg: 'en', lc: 'LS' }, // Lesotho (Vodacom)
|
|
467
|
+
'267': { mcc: '652', mnc: '01', lg: 'en', lc: 'BW' }, // Botswana (Mascom)
|
|
468
|
+
'268': { mcc: '653', mnc: '02', lg: 'en', lc: 'SZ' }, // Eswatini (Eswatini Mobile Li)
|
|
469
|
+
'269': { mcc: '654', mnc: '01', lg: 'ar', lc: 'KM' }, // Comoros (HURI)
|
|
470
|
+
'290': { mcc: '658', mnc: '01', lg: 'en', lc: 'SH' }, // Saint Helena, Ascension and Tristan da Cunha (Sure)
|
|
471
|
+
'291': { mcc: '657', mnc: '01', lg: 'ar', lc: 'ER' }, // Eritrea (Eritel)
|
|
472
|
+
'297': { mcc: '363', mnc: '01', lg: 'nl', lc: 'AW' }, // Aruba (SETAR)
|
|
473
|
+
'298': { mcc: '288', mnc: '01', lg: 'da', lc: 'FO' }, // Faroe Islands (Føroya Tele)
|
|
474
|
+
'299': { mcc: '290', mnc: '01', lg: 'kl', lc: 'GL' }, // Greenland (tusass)
|
|
475
|
+
'350': { mcc: '266', mnc: '01', lg: 'en', lc: 'GI' }, // Gibraltar (GibTel)
|
|
476
|
+
'352': { mcc: '270', mnc: '01', lg: 'de', lc: 'LU' }, // Luxembourg (POST)
|
|
477
|
+
'354': { mcc: '274', mnc: '01', lg: 'is', lc: 'IS' }, // Iceland (Síminn)
|
|
478
|
+
'355': { mcc: '276', mnc: '01', lg: 'sq', lc: 'AL' }, // Albania (ONE)
|
|
479
|
+
'356': { mcc: '278', mnc: '01', lg: 'en', lc: 'MT' }, // Malta (Epic)
|
|
480
|
+
'357': { mcc: '280', mnc: '01', lg: 'el', lc: 'CY' }, // Cyprus (Cytamobile-Vodafon)
|
|
481
|
+
'359': { mcc: '284', mnc: '01', lg: 'bg', lc: 'BG' }, // Bulgaria (A1 BG)
|
|
482
|
+
'370': { mcc: '246', mnc: '01', lg: 'lt', lc: 'LT' }, // Lithuania (Telia)
|
|
483
|
+
'371': { mcc: '247', mnc: '01', lg: 'lv', lc: 'LV' }, // Latvia (LMT)
|
|
484
|
+
'372': { mcc: '248', mnc: '01', lg: 'et', lc: 'EE' }, // Estonia (Telia)
|
|
485
|
+
'373': { mcc: '259', mnc: '01', lg: 'ro', lc: 'MD' }, // Moldova (Orange)
|
|
486
|
+
'374': { mcc: '283', mnc: '01', lg: 'hy', lc: 'AM' }, // Armenia (Beeline)
|
|
487
|
+
'375': { mcc: '257', mnc: '01', lg: 'be', lc: 'BY' }, // Belarus (A1)
|
|
488
|
+
'376': { mcc: '213', mnc: '03', lg: 'ca', lc: 'AD' }, // Andorra (Som, Mobiland)
|
|
489
|
+
'377': { mcc: '212', mnc: '10', lg: 'fr', lc: 'MC' }, // Monaco (Office des Telepho)
|
|
490
|
+
'378': { mcc: '292', mnc: '01', lg: 'it', lc: 'SM' }, // San Marino (PRIMA)
|
|
491
|
+
'381': { mcc: '220', mnc: '01', lg: 'sr', lc: 'RS' }, // Serbia (Yettel)
|
|
492
|
+
'382': { mcc: '297', mnc: '01', lg: 'en', lc: 'ME' }, // Montenegro (One)
|
|
493
|
+
'383': { mcc: '221', mnc: '01', lg: 'sq', lc: 'XK' }, // Kosovo (Vala)
|
|
494
|
+
'385': { mcc: '219', mnc: '01', lg: 'hr', lc: 'HR' }, // Croatia (HT HR)
|
|
495
|
+
'386': { mcc: '293', mnc: '10', lg: 'sl', lc: 'SI' }, // Slovenia (SŽ - Infrastruktur)
|
|
496
|
+
'387': { mcc: '218', mnc: '03', lg: 'bs', lc: 'BA' }, // Bosnia and Herzegovina (HT-ERONET)
|
|
497
|
+
'389': { mcc: '294', mnc: '01', lg: 'mk', lc: 'MK' }, // North Macedonia (Telekom.mk)
|
|
498
|
+
'421': { mcc: '231', mnc: '01', lg: 'sk', lc: 'SK' }, // Slovakia (Orange)
|
|
499
|
+
'423': { mcc: '295', mnc: '01', lg: 'de', lc: 'LI' }, // Liechtenstein (Swisscom)
|
|
500
|
+
'500': { mcc: '750', mnc: '001', lg: 'en', lc: 'FK' }, // Falkland Islands (Sure)
|
|
501
|
+
'501': { mcc: '702', mnc: '67', lg: 'en', lc: 'BZ' }, // Belize (DigiCell)
|
|
502
|
+
'502': { mcc: '704', mnc: '01', lg: 'es', lc: 'GT' }, // Guatemala (Claro)
|
|
503
|
+
'503': { mcc: '706', mnc: '01', lg: 'es', lc: 'SV' }, // El Salvador (Claro)
|
|
504
|
+
'504': { mcc: '708', mnc: '001', lg: 'es', lc: 'HN' }, // Honduras (Claro)
|
|
505
|
+
'505': { mcc: '710', mnc: '21', lg: 'es', lc: 'NI' }, // Nicaragua (Claro)
|
|
506
|
+
'506': { mcc: '712', mnc: '01', lg: 'es', lc: 'CR' }, // Costa Rica (Kölbi ICE)
|
|
507
|
+
'507': { mcc: '714', mnc: '01', lg: 'es', lc: 'PA' }, // Panama (Cable & Wireless)
|
|
508
|
+
'508': { mcc: '308', mnc: '01', lg: 'fr', lc: 'PM' }, // Saint Pierre and Miquelon (Ameris)
|
|
509
|
+
'509': { mcc: '372', mnc: '02', lg: 'fr', lc: 'HT' }, // Haiti (Digicel)
|
|
510
|
+
'591': { mcc: '736', mnc: '01', lg: 'ay', lc: 'BO' }, // Bolivia (Viva)
|
|
511
|
+
'592': { mcc: '738', mnc: '00', lg: 'en', lc: 'GY' }, // Guyana (E-Networks)
|
|
512
|
+
'593': { mcc: '740', mnc: '00', lg: 'es', lc: 'EC' }, // Ecuador (Movistar)
|
|
513
|
+
'594': { mcc: '340', mnc: '01', lg: 'fr', lc: 'GF' }, // French Guiana (Orange)
|
|
514
|
+
'595': { mcc: '744', mnc: '01', lg: 'gn', lc: 'PY' }, // Paraguay (VOX)
|
|
515
|
+
'597': { mcc: '746', mnc: '02', lg: 'nl', lc: 'SR' }, // Suriname (Telesur)
|
|
516
|
+
'598': { mcc: '748', mnc: '01', lg: 'es', lc: 'UY' }, // Uruguay (Antel)
|
|
517
|
+
'670': { mcc: '514', mnc: '01', lg: 'pt', lc: 'TL' }, // Timor-Leste (Telkomcel)
|
|
518
|
+
'672': { mcc: '505', mnc: '10', lg: 'en', lc: 'NF' }, // Norfolk Island (Norfolk Telecom)
|
|
519
|
+
'673': { mcc: '528', mnc: '02', lg: 'ms', lc: 'BN' }, // Brunei (PCSB)
|
|
520
|
+
'674': { mcc: '536', mnc: '02', lg: 'en', lc: 'NR' }, // Nauru (Digicel)
|
|
521
|
+
'675': { mcc: '537', mnc: '01', lg: 'en', lc: 'PG' }, // Papua New Guinea (bmobile)
|
|
522
|
+
'676': { mcc: '539', mnc: '01', lg: 'en', lc: 'TO' }, // Tonga (U-Call)
|
|
523
|
+
'677': { mcc: '540', mnc: '01', lg: 'en', lc: 'SB' }, // Solomon Islands (BREEZE)
|
|
524
|
+
'678': { mcc: '541', mnc: '00', lg: 'bi', lc: 'VU' }, // Vanuatu (AIL)
|
|
525
|
+
'679': { mcc: '542', mnc: '01', lg: 'en', lc: 'FJ' }, // Fiji (Vodafone)
|
|
526
|
+
'680': { mcc: '552', mnc: '01', lg: 'en', lc: 'PW' }, // Palau (PNCC)
|
|
527
|
+
'681': { mcc: '543', mnc: '01', lg: 'fr', lc: 'WF' }, // Wallis and Futuna (Manuia)
|
|
528
|
+
'682': { mcc: '548', mnc: '01', lg: 'en', lc: 'CK' }, // Cook Islands (Vodafone)
|
|
529
|
+
'683': { mcc: '555', mnc: '01', lg: 'en', lc: 'NU' }, // Niue (Telecom Niue)
|
|
530
|
+
'685': { mcc: '549', mnc: '01', lg: 'en', lc: 'WS' }, // Samoa (Digicel)
|
|
531
|
+
'686': { mcc: '545', mnc: '01', lg: 'en', lc: 'KI' }, // Kiribati (Kiribati - ATH)
|
|
532
|
+
'687': { mcc: '546', mnc: '01', lg: 'fr', lc: 'NC' }, // New Caledonia (Mobilis)
|
|
533
|
+
'688': { mcc: '553', mnc: '01', lg: 'en', lc: 'TV' }, // Tuvalu (TTC)
|
|
534
|
+
'689': { mcc: '547', mnc: '05', lg: 'fr', lc: 'PF' }, // French Polynesia (Ora)
|
|
535
|
+
'690': { mcc: '554', mnc: '01', lg: 'en', lc: 'TK' }, // Tokelau (Teletok)
|
|
536
|
+
'691': { mcc: '550', mnc: '01', lg: 'en', lc: 'FM' }, // Micronesia (FSMTC)
|
|
537
|
+
'692': { mcc: '551', mnc: '01', lg: 'en', lc: 'MH' }, // Marshall Islands (Marshall Islands N)
|
|
538
|
+
'850': { mcc: '467', mnc: '05', lg: 'ko', lc: 'KP' }, // North Korea (Koryolink)
|
|
539
|
+
'852': { mcc: '454', mnc: '00', lg: 'en', lc: 'HK' }, // Hong Kong (1O1O / One2Free / )
|
|
540
|
+
'853': { mcc: '455', mnc: '00', lg: 'pt', lc: 'MO' }, // Macau (SmarTone)
|
|
541
|
+
'855': { mcc: '456', mnc: '01', lg: 'km', lc: 'KH' }, // Cambodia (Cellcard)
|
|
542
|
+
'856': { mcc: '457', mnc: '01', lg: 'lo', lc: 'LA' }, // Laos (LaoTel)
|
|
543
|
+
'886': { mcc: '466', mnc: '01', lg: 'zh', lc: 'TW' }, // Taiwan (FarEasTone)
|
|
544
|
+
'960': { mcc: '472', mnc: '01', lg: 'dv', lc: 'MV' }, // Maldives (Dhiraagu)
|
|
545
|
+
'961': { mcc: '415', mnc: '01', lg: 'ar', lc: 'LB' }, // Lebanon (Alfa)
|
|
546
|
+
'962': { mcc: '416', mnc: '01', lg: 'ar', lc: 'JO' }, // Jordan (zain JO)
|
|
547
|
+
'963': { mcc: '417', mnc: '01', lg: 'ar', lc: 'SY' }, // Syria (Syriatel)
|
|
548
|
+
'964': { mcc: '418', mnc: '00', lg: 'ar', lc: 'IQ' }, // Iraq (Asia Cell)
|
|
549
|
+
'965': { mcc: '419', mnc: '02', lg: 'ar', lc: 'KW' }, // Kuwait (zain KW)
|
|
550
|
+
'967': { mcc: '421', mnc: '01', lg: 'ar', lc: 'YE' }, // Yemen (SabaFon)
|
|
551
|
+
'968': { mcc: '422', mnc: '02', lg: 'ar', lc: 'OM' }, // Oman (Omantel)
|
|
552
|
+
'970': { mcc: '425', mnc: '05', lg: 'ar', lc: 'PS' }, // Palestine (Jawwal)
|
|
553
|
+
'973': { mcc: '426', mnc: '01', lg: 'ar', lc: 'BH' }, // Bahrain (Batelco)
|
|
554
|
+
'974': { mcc: '427', mnc: '01', lg: 'ar', lc: 'QA' }, // Qatar (Ooredoo)
|
|
555
|
+
'975': { mcc: '402', mnc: '11', lg: 'dz', lc: 'BT' }, // Bhutan (B-Mobile)
|
|
556
|
+
'976': { mcc: '428', mnc: '88', lg: 'mn', lc: 'MN' }, // Mongolia (Unitel)
|
|
557
|
+
'977': { mcc: '429', mnc: '01', lg: 'ne', lc: 'NP' }, // Nepal (Namaste / NT Mobil)
|
|
558
|
+
'992': { mcc: '436', mnc: '01', lg: 'ru', lc: 'TJ' }, // Tajikistan (Tcell)
|
|
559
|
+
'993': { mcc: '438', mnc: '02', lg: 'ru', lc: 'TM' }, // Turkmenistan (TM-Cell)
|
|
560
|
+
'994': { mcc: '400', mnc: '01', lg: 'az', lc: 'AZ' }, // Azerbaijan (Azercell)
|
|
561
|
+
'995': { mcc: '282', mnc: '01', lg: 'ka', lc: 'GE' }, // Georgia (Geocell)
|
|
562
|
+
'996': { mcc: '437', mnc: '01', lg: 'ky', lc: 'KG' }, // Kyrgyzstan (Beeline)
|
|
563
|
+
'998': { mcc: '434', mnc: '03', lg: 'ru', lc: 'UZ' }, // Uzbekistan (UzMobile)
|
|
421
564
|
};
|
|
422
565
|
|
|
423
566
|
function getCountryMeta(cc) {
|
|
@@ -431,17 +574,20 @@ function getCountryMeta(cc) {
|
|
|
431
574
|
// (Java long conversion removes them automatically)
|
|
432
575
|
|
|
433
576
|
const THREE_DIGIT_CCS = new Set([
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
'
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'
|
|
441
|
-
'
|
|
442
|
-
'
|
|
443
|
-
'
|
|
444
|
-
'
|
|
577
|
+
'211','212','213','216','218','220','221','222','223','224','225','226',
|
|
578
|
+
'227','228','229','230','231','232','233','234','235','236','237','238',
|
|
579
|
+
'239','240','241','242','243','244','245','246','247','248','249','250',
|
|
580
|
+
'251','252','253','254','255','256','257','258','260','261','262','263',
|
|
581
|
+
'264','265','266','267','268','269','290','291','297','298','299','350',
|
|
582
|
+
'351','352','353','354','355','356','357','358','359','370','371','372',
|
|
583
|
+
'373','374','375','376','377','378','380','381','382','383','385','386',
|
|
584
|
+
'387','389','420','421','423','500','501','502','503','504','505','506',
|
|
585
|
+
'507','508','509','590','591','592','593','594','595','596','597','598',
|
|
586
|
+
'599','670','672','673','674','675','676','677','678','679','680','681',
|
|
587
|
+
'682','683','685','686','687','688','689','690','691','692','850','852',
|
|
588
|
+
'853','855','856','880','886','960','961','962','963','964','965','966',
|
|
589
|
+
'967','968','970','971','972','973','974','975','976','977','992','993',
|
|
590
|
+
'994','995','996','998'
|
|
445
591
|
]);
|
|
446
592
|
|
|
447
593
|
const TWO_DIGIT_CCS = new Set([
|
|
@@ -451,11 +597,10 @@ const TWO_DIGIT_CCS = new Set([
|
|
|
451
597
|
'95','98'
|
|
452
598
|
]);
|
|
453
599
|
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
// country table itself gets the last word.
|
|
600
|
+
// The country table only covers the places we carry carrier metadata for, which
|
|
601
|
+
// is far fewer than the places that can register. Anything it names still has to
|
|
602
|
+
// be splittable, so it is folded in — a no-op while the lists above are complete,
|
|
603
|
+
// and a backstop if a code is ever added to the table and not to them.
|
|
459
604
|
for (const cc of Object.keys(COUNTRY_META)) {
|
|
460
605
|
if (cc.length === 3) THREE_DIGIT_CCS.add(cc);
|
|
461
606
|
else if (cc.length === 2) TWO_DIGIT_CCS.add(cc);
|
package/lib/image/Bmp.js
CHANGED
|
@@ -13,15 +13,27 @@ function isBmp(buf) {
|
|
|
13
13
|
// One channel out of a packed pixel. A mask says where the channel sits and how
|
|
14
14
|
// wide it is, and the width varies — 5-6-5 and 5-5-5 are both ordinary — so the
|
|
15
15
|
// value is scaled by its own maximum rather than shifted by a fixed amount.
|
|
16
|
+
//
|
|
17
|
+
// Both scans below stop at 32 on their own, because a shift count in JS is
|
|
18
|
+
// taken mod 32: `bits >>> 32` is `bits >>> 0`, so a mask of all ones sends an
|
|
19
|
+
// unbounded scan back to bit 0 and it never comes out. Nothing upstream could
|
|
20
|
+
// catch that — decodeImage's try/catch cannot interrupt a loop that does not
|
|
21
|
+
// end, and a thumbnail is built on the way to sending an ordinary image, so
|
|
22
|
+
// one crafted BMP would take the whole process down with it.
|
|
16
23
|
function maskChannel(v, mask) {
|
|
17
24
|
if (!mask) return 0;
|
|
18
25
|
let shift = 0;
|
|
19
|
-
while (!((mask >>> shift) & 1)) shift++;
|
|
26
|
+
while (shift < 32 && !((mask >>> shift) & 1)) shift++;
|
|
27
|
+
if (shift === 32) return 0; // no bit set — nothing to read
|
|
20
28
|
const bits = (mask >>> shift) >>> 0;
|
|
21
29
|
let width = 0;
|
|
22
|
-
while ((bits >>> width) & 1) width++;
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
while (width < 32 && ((bits >>> width) & 1)) width++;
|
|
31
|
+
// The obvious `(1 << width) - 1` is wrong at both ends of that range: `1 <<
|
|
32
|
+
// 32` is 1, and `1 << 31` is negative, so a 31- or 32-bit channel would be
|
|
33
|
+
// scaled by zero or by a negative maximum. Build the maximum unsigned, and
|
|
34
|
+
// read the value back unsigned too — `& 0xffffffff` alone yields an int32.
|
|
35
|
+
const max = width >= 32 ? 0xffffffff : ((1 << width) >>> 0) - 1;
|
|
36
|
+
return Math.round((((v >>> shift) & max) >>> 0) * 255 / max);
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
function decodeBmp(buf) {
|
|
@@ -14,6 +14,27 @@ const { SessionRecord } = require('./libsignal');
|
|
|
14
14
|
// • on process exit, flush synchronously (so state is never lost)
|
|
15
15
|
//
|
|
16
16
|
// Result: zero blocking disk I/O during the encryption hot-path.
|
|
17
|
+
//
|
|
18
|
+
// Two rules keep that from costing the account everything it knows:
|
|
19
|
+
//
|
|
20
|
+
// • Every write goes to a temp file and is renamed into place. Writing over
|
|
21
|
+
// the live path is not one step — it is an open, a truncate and as many
|
|
22
|
+
// writes as the file needs — and anything that reads or writes in the
|
|
23
|
+
// middle of that sees a mixture of two snapshots. That mixture is not
|
|
24
|
+
// valid JSON, and _load() below answers unreadable state with an empty
|
|
25
|
+
// store, so a single interleaved write silently costs the account every
|
|
26
|
+
// session, pre-key, identity and LID mapping it had. rename(2) is atomic:
|
|
27
|
+
// the file is one whole snapshot or the other, never a blend.
|
|
28
|
+
//
|
|
29
|
+
// • A write never lands if newer state reached the disk while it was in
|
|
30
|
+
// flight. The async write started by the debounce is not cancellable, and
|
|
31
|
+
// a synchronous flush — detach(), or the exit handlers — can easily
|
|
32
|
+
// overtake it; letting the older snapshot finish last would undo every
|
|
33
|
+
// session written since. _writtenRev records what the file holds, and a
|
|
34
|
+
// write that has been overtaken drops its temp file instead of renaming.
|
|
35
|
+
//
|
|
36
|
+
// In-memory state is always the newest thing there is, so any write we abandon
|
|
37
|
+
// costs nothing: the next one writes what is current.
|
|
17
38
|
|
|
18
39
|
const DEBOUNCE_MS = 300;
|
|
19
40
|
|
|
@@ -37,6 +58,15 @@ class SignalStore {
|
|
|
37
58
|
this._dirty = false;
|
|
38
59
|
this._rev = 0;
|
|
39
60
|
this._saveTimer = null;
|
|
61
|
+
// The revision the file on disk actually holds. An async write compares
|
|
62
|
+
// against it before renaming: if a synchronous flush got there first, the
|
|
63
|
+
// write it is holding is stale and must not land.
|
|
64
|
+
this._writtenRev = 0;
|
|
65
|
+
// One async write at a time. Two of them running together on one path
|
|
66
|
+
// interleave, and _writeAgain remembers that another one came due while
|
|
67
|
+
// the first was still going so it can be re-run from a fresh snapshot.
|
|
68
|
+
this._writing = false;
|
|
69
|
+
this._writeAgain = false;
|
|
40
70
|
|
|
41
71
|
// Flush synchronously on exit so no state is ever lost.
|
|
42
72
|
//
|
|
@@ -61,7 +91,14 @@ class SignalStore {
|
|
|
61
91
|
process.removeListener('SIGTERM', this._onSigTerm);
|
|
62
92
|
process.removeListener('SIGINT', this._onSigInt);
|
|
63
93
|
} catch (_) {}
|
|
64
|
-
|
|
94
|
+
// Clearing the path is also what tells a write that is still in flight to
|
|
95
|
+
// stand down — it checks the path it started with before renaming. The
|
|
96
|
+
// caller detaches in order to move these files, so leave nothing of ours
|
|
97
|
+
// behind in the directory it is about to rename.
|
|
98
|
+
const stale = this._filePath ? [this._tmpPath(true), this._tmpPath(false)] : [];
|
|
99
|
+
this._filePath = null;
|
|
100
|
+
this._writeAgain = false;
|
|
101
|
+
for (const f of stale) { try { fs.unlinkSync(f); } catch (_) {} }
|
|
65
102
|
}
|
|
66
103
|
|
|
67
104
|
init(identityKeyPair, registrationId) {
|
|
@@ -100,23 +137,67 @@ class SignalStore {
|
|
|
100
137
|
return this._lidMappings;
|
|
101
138
|
}
|
|
102
139
|
|
|
103
|
-
//
|
|
140
|
+
// Everything that is persisted, as it goes on disk.
|
|
141
|
+
_snapshot() {
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
sessions: this._sessions,
|
|
144
|
+
preKeys: this._preKeys,
|
|
145
|
+
signedPreKeys: this._signedPreKeys,
|
|
146
|
+
identities: this._identities,
|
|
147
|
+
lidMappings: this._lidMappings
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Where a write assembles the file before it becomes the file. The sync and
|
|
152
|
+
// async writers get separate names so the one can never find the other's
|
|
153
|
+
// half-written temp file under it.
|
|
154
|
+
_tmpPath(sync) {
|
|
155
|
+
return this._filePath + (sync ? '.stmp' : '.atmp');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A rename replaces the file, so the one that lands carries the temp file's
|
|
159
|
+
// permissions and not the old file's. This holds Signal private keys, and an
|
|
160
|
+
// operator who tightened its mode would find it loosened again by the next
|
|
161
|
+
// write — so carry whatever the file already had across. null when there is
|
|
162
|
+
// no file yet, which leaves the default the old writes produced.
|
|
163
|
+
_modeOf(file) {
|
|
164
|
+
try { return fs.statSync(file).mode & 0o777; } catch (_) { return null; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Immediate synchronous flush (process exit, and detach())
|
|
104
168
|
_flushSync() {
|
|
105
169
|
if (!this._dirty || !this._filePath) return;
|
|
106
170
|
if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
|
|
171
|
+
const rev = this._rev;
|
|
172
|
+
const file = this._filePath;
|
|
173
|
+
const tmp = this._tmpPath(true);
|
|
107
174
|
try {
|
|
108
|
-
const
|
|
109
|
-
sessions: this._sessions,
|
|
110
|
-
preKeys: this._preKeys,
|
|
111
|
-
signedPreKeys: this._signedPreKeys,
|
|
112
|
-
identities: this._identities,
|
|
113
|
-
lidMappings: this._lidMappings
|
|
114
|
-
});
|
|
115
|
-
const dir = path.dirname(this._filePath);
|
|
175
|
+
const dir = path.dirname(file);
|
|
116
176
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
117
|
-
|
|
118
|
-
this.
|
|
119
|
-
|
|
177
|
+
const mode = this._modeOf(file);
|
|
178
|
+
fs.writeFileSync(tmp, this._snapshot(), mode !== null
|
|
179
|
+
? { encoding: 'utf8', mode } : 'utf8');
|
|
180
|
+
if (mode !== null) fs.chmodSync(tmp, mode);
|
|
181
|
+
fs.renameSync(tmp, file);
|
|
182
|
+
// Take the async writer's temp file away from it.
|
|
183
|
+
//
|
|
184
|
+
// Its "have I been overtaken?" check runs on this thread, but the rename
|
|
185
|
+
// it authorises is carried out on a pool thread afterwards — and at exit
|
|
186
|
+
// the pool is usually busy, so that rename can still be queued now and
|
|
187
|
+
// execute during teardown, putting this snapshot back to an older one.
|
|
188
|
+
// Its callback never runs there, so nothing would notice or repair it.
|
|
189
|
+
// Removing the source makes that queued rename fail ENOENT instead.
|
|
190
|
+
// detach() does the same thing, which is why detach() was never exposed
|
|
191
|
+
// to this; the exit path simply was not doing it.
|
|
192
|
+
try { fs.unlinkSync(this._tmpPath(false)); } catch (_) {}
|
|
193
|
+
// Nothing ran between the snapshot and the rename — this is synchronous —
|
|
194
|
+
// so the file now holds exactly this revision and there is nothing left
|
|
195
|
+
// to write.
|
|
196
|
+
this._writtenRev = rev;
|
|
197
|
+
this._dirty = false;
|
|
198
|
+
} catch (_) {
|
|
199
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
200
|
+
}
|
|
120
201
|
}
|
|
121
202
|
|
|
122
203
|
// Schedule an async (non-blocking) write after DEBOUNCE_MS inactivity.
|
|
@@ -128,25 +209,67 @@ class SignalStore {
|
|
|
128
209
|
if (this._saveTimer) clearTimeout(this._saveTimer);
|
|
129
210
|
this._saveTimer = setTimeout(() => {
|
|
130
211
|
this._saveTimer = null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
212
|
+
this._writeAsync();
|
|
213
|
+
}, DEBOUNCE_MS);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The debounced write itself: temp file, then rename into place, and only if
|
|
217
|
+
// nothing newer reached the disk in the meantime.
|
|
218
|
+
_writeAsync() {
|
|
219
|
+
if (!this._filePath || !this._dirty) return;
|
|
220
|
+
// Fold a write that comes due mid-write into the one already running
|
|
221
|
+
// rather than starting a second one over the same temp file.
|
|
222
|
+
if (this._writing) { this._writeAgain = true; return; }
|
|
223
|
+
|
|
224
|
+
// The revision this write is a snapshot of, and the path it is for. Both
|
|
225
|
+
// are read once, here: by the time the callbacks run either may have moved
|
|
226
|
+
// on, and that is exactly what they have to notice.
|
|
227
|
+
const rev = this._rev;
|
|
228
|
+
const file = this._filePath;
|
|
229
|
+
const tmp = this._tmpPath(false);
|
|
230
|
+
|
|
231
|
+
let data, mode;
|
|
232
|
+
try {
|
|
233
|
+
const dir = path.dirname(file);
|
|
141
234
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
235
|
+
mode = this._modeOf(file);
|
|
236
|
+
data = this._snapshot();
|
|
237
|
+
} catch (_) { return; }
|
|
238
|
+
|
|
239
|
+
this._writing = true;
|
|
240
|
+
this._writeAgain = false;
|
|
241
|
+
|
|
242
|
+
const finish = () => {
|
|
243
|
+
this._writing = false;
|
|
244
|
+
// A write came due while this one was running and was folded into it.
|
|
245
|
+
// Every other mutation armed its own debounce timer, which is still
|
|
246
|
+
// pending and will write what is current.
|
|
247
|
+
if (this._writeAgain && this._dirty && this._filePath) this._writeAsync();
|
|
248
|
+
};
|
|
249
|
+
const abandon = () => { fs.unlink(tmp, () => {}); finish(); };
|
|
250
|
+
|
|
251
|
+
// Create the temp file already tight rather than widening the window in
|
|
252
|
+
// which a store full of private keys sits at the umask default.
|
|
253
|
+
fs.writeFile(tmp, data, mode !== null ? { encoding: 'utf8', mode } : 'utf8', (err) => {
|
|
254
|
+
if (err) return abandon();
|
|
255
|
+
// detach() let the file go, or a synchronous flush wrote newer state
|
|
256
|
+
// while this one was running. Renaming now would put an older snapshot
|
|
257
|
+
// back and take every session written since with it.
|
|
258
|
+
if (file !== this._filePath || rev <= this._writtenRev) return abandon();
|
|
259
|
+
if (mode !== null) { try { fs.chmodSync(tmp, mode); } catch (_) {} }
|
|
260
|
+
fs.rename(tmp, file, (renameErr) => {
|
|
261
|
+
if (renameErr) return abandon();
|
|
262
|
+
// Never walk it backwards: a synchronous flush can land — and record a
|
|
263
|
+
// higher revision — between the check above and this callback.
|
|
264
|
+
if (rev > this._writtenRev) this._writtenRev = rev;
|
|
265
|
+
// Only clear the flag when nothing changed while the write was
|
|
266
|
+
// running. A mutation that arrived after the snapshot is not in
|
|
267
|
+
// `data`, and clearing here would make the exit-time _flushSync()
|
|
268
|
+
// skip it and lose it.
|
|
147
269
|
if (this._rev === rev) this._dirty = false;
|
|
270
|
+
finish();
|
|
148
271
|
});
|
|
149
|
-
}
|
|
272
|
+
});
|
|
150
273
|
}
|
|
151
274
|
|
|
152
275
|
// ─── Identity / registration ─────────────────────────────────────────────
|