Major re-write of decodeModesMessage
Lots of changes to the decodeModesMessages() function, and associated helper functions, with the aim of speeding the code up by reducing the number of calls to the CRC functions. Also, only update the mm variables which are part of the message we're decoding. Change the way CRC's are generated and checked. Change the way single and double bit error is implemented. General tidy up of the function to make it more readable.
This commit is contained in:
parent
e9b463d25c
commit
50cedd7fe2
326
dump1090.c
326
dump1090.c
|
@ -303,8 +303,8 @@ void modesSendRawOutput(struct modesMessage *mm);
|
|||
void modesSendBeastOutput(struct modesMessage *mm);
|
||||
void modesSendSBSOutput(struct modesMessage *mm, struct aircraft *a);
|
||||
void useModesMessage(struct modesMessage *mm);
|
||||
int fixSingleBitErrors(unsigned char *msg, int bits, struct modesMessage *mm);
|
||||
int fixTwoBitsErrors(unsigned char *msg, int bits, struct modesMessage *mm);
|
||||
int fixSingleBitErrors(unsigned char *msg, int bits);
|
||||
int fixTwoBitsErrors(unsigned char *msg, int bits);
|
||||
int modesMessageLenByType(int type);
|
||||
|
||||
/* ============================= Utility functions ========================== */
|
||||
|
@ -700,15 +700,13 @@ void dumpRawMessage(char *descr, unsigned char *msg,
|
|||
uint16_t *m, uint32_t offset)
|
||||
{
|
||||
int j;
|
||||
int msgtype = msg[0]>>3;
|
||||
int msgtype = msg[0] >> 3;
|
||||
int fixable = -1;
|
||||
|
||||
if (msgtype == 11 || msgtype == 17) {
|
||||
int msgbits = (msgtype == 11) ? MODES_SHORT_MSG_BITS :
|
||||
MODES_LONG_MSG_BITS;
|
||||
fixable = fixSingleBitErrors(msg, msgbits, NULL);
|
||||
if (msgtype == 17) {
|
||||
fixable = fixSingleBitErrors(msg, MODES_LONG_MSG_BITS);
|
||||
if (fixable == -1)
|
||||
fixable = fixTwoBitsErrors(msg, msgbits, NULL);
|
||||
fixable = fixTwoBitsErrors(msg, MODES_LONG_MSG_BITS);
|
||||
}
|
||||
|
||||
if (Modes.debug & MODES_DEBUG_JS) {
|
||||
|
@ -1111,6 +1109,7 @@ uint32_t modes_checksum_table[112] = {
|
|||
|
||||
uint32_t modesChecksum(unsigned char *msg, int bits) {
|
||||
uint32_t crc = 0;
|
||||
uint32_t rem = 0;
|
||||
int offset = (bits == 112) ? 0 : (112-56);
|
||||
uint8_t theByte = *msg;
|
||||
uint32_t * pCRCTable = &modes_checksum_table[offset];
|
||||
|
@ -1118,14 +1117,14 @@ uint32_t modesChecksum(unsigned char *msg, int bits) {
|
|||
|
||||
for(j = 0; j < bits; j++) {
|
||||
if ((j & 7) == 0)
|
||||
{theByte = *msg++;}
|
||||
{theByte = *msg++; rem = (rem << 8) | theByte;}
|
||||
|
||||
// If bit is set, xor with corresponding table entry.
|
||||
if (theByte & 0x80) {crc ^= *pCRCTable;}
|
||||
pCRCTable++;
|
||||
theByte = theByte << 1;
|
||||
}
|
||||
return crc; // 24 bit checksum.
|
||||
return ((crc ^ rem) & 0x00FFFFFF); // 24 bit checksum.
|
||||
}
|
||||
//
|
||||
// Given the Downlink Format (DF) of the message, return the message length in bits.
|
||||
|
@ -1137,97 +1136,79 @@ uint32_t modesChecksum(unsigned char *msg, int bits) {
|
|||
int modesMessageLenByType(int type) {
|
||||
return (type & 0x10) ? MODES_LONG_MSG_BITS : MODES_SHORT_MSG_BITS ;
|
||||
}
|
||||
|
||||
/* Try to fix single bit errors using the checksum. On success modifies
|
||||
* the original buffer with the fixed version, and returns the position
|
||||
* of the error bit. Otherwise if fixing failed -1 is returned. */
|
||||
int fixSingleBitErrors(unsigned char *msg, int bits, struct modesMessage *mm) {
|
||||
//
|
||||
// Try to fix single bit errors using the checksum. On success modifies
|
||||
// the original buffer with the fixed version, and returns the position
|
||||
// of the error bit. Otherwise if fixing failed -1 is returned.
|
||||
//
|
||||
int fixSingleBitErrors(unsigned char *msg, int bits) {
|
||||
int j;
|
||||
unsigned char aux[MODES_LONG_MSG_BYTES];
|
||||
|
||||
memcpy(aux, msg,bits/8);
|
||||
memcpy(aux, msg, bits/8);
|
||||
|
||||
for (j = 0; j < bits; j++) {
|
||||
int byte = j/8;
|
||||
int bitmask = 1 << (7-(j%8));
|
||||
uint32_t crc1, crc2;
|
||||
// Do not attempt to error correct Bits 0-4. These contain the DF, and must
|
||||
// be correct because we can only error correct DF17
|
||||
for (j = 5; j < bits; j++) {
|
||||
int byte = j/8;
|
||||
int bitmask = 1 << (7 - (j & 7));
|
||||
|
||||
aux[byte] ^= bitmask; /* Flip j-th bit. */
|
||||
aux[byte] ^= bitmask; // Flip j-th bit
|
||||
|
||||
crc1 = ((uint32_t)aux[(bits/8)-3] << 16) |
|
||||
((uint32_t)aux[(bits/8)-2] << 8) |
|
||||
(uint32_t)aux[(bits/8)-1];
|
||||
crc2 = modesChecksum(aux,bits);
|
||||
|
||||
if (crc1 == crc2) {
|
||||
/* The error is fixed. Overwrite the original buffer with
|
||||
* the corrected sequence, and returns the error bit
|
||||
* position. */
|
||||
memcpy(msg,aux,bits/8);
|
||||
if (mm)
|
||||
{
|
||||
mm->crc = crc2;
|
||||
mm->iid = 0;
|
||||
mm->crcok = 1;
|
||||
}
|
||||
return j;
|
||||
if (0 == modesChecksum(aux, bits)) {
|
||||
// The error is fixed. Overwrite the original buffer with the
|
||||
// corrected sequence, and returns the error bit position
|
||||
msg[byte] = aux[byte];
|
||||
return (j);
|
||||
}
|
||||
|
||||
aux[byte] ^= bitmask; /* Flip j-th bit back again. */
|
||||
aux[byte] ^= bitmask; // Flip j-th bit back again
|
||||
}
|
||||
return -1;
|
||||
return (-1);
|
||||
}
|
||||
|
||||
/* Similar to fixSingleBitErrors() but try every possible two bit combination.
|
||||
* This is very slow and should be tried only against DF17 messages that
|
||||
* don't pass the checksum, and only in Aggressive Mode. */
|
||||
int fixTwoBitsErrors(unsigned char *msg, int bits, struct modesMessage *mm) {
|
||||
//
|
||||
// Similar to fixSingleBitErrors() but try every possible two bit combination.
|
||||
// This is very slow and should be tried only against DF17 messages that
|
||||
// don't pass the checksum, and only in Aggressive Mode.
|
||||
//
|
||||
int fixTwoBitsErrors(unsigned char *msg, int bits) {
|
||||
int j, i;
|
||||
unsigned char aux[MODES_LONG_MSG_BYTES];
|
||||
|
||||
memcpy(aux,msg, bits/8);
|
||||
memcpy(aux, msg, bits/8);
|
||||
|
||||
for (j = 0; j < bits; j++) {
|
||||
int byte1 = j/8;
|
||||
int bitmask1 = 1 << (7-(j%8));
|
||||
aux[byte1] ^= bitmask1; /* Flip j-th bit. */
|
||||
// Do not attempt to error correct Bits 0-4. These contain the DF, and must
|
||||
// be correct because we can only error correct DF17
|
||||
for (j = 5; j < bits; j++) {
|
||||
int byte1 = j/8;
|
||||
int bitmask1 = 1 << (7 - (j & 7));
|
||||
aux[byte1] ^= bitmask1; // Flip j-th bit
|
||||
|
||||
/* Don't check the same pairs multiple times, so i starts from j+1 */
|
||||
// Don't check the same pairs multiple times, so i starts from j+1
|
||||
for (i = j+1; i < bits; i++) {
|
||||
int byte2 = i/8;
|
||||
int bitmask2 = 1 << (7-(i%8));
|
||||
uint32_t crc1, crc2;
|
||||
int byte2 = i/8;
|
||||
int bitmask2 = 1 << (7 - (i & 7));
|
||||
|
||||
aux[byte2] ^= bitmask2; /* Flip i-th bit. */
|
||||
aux[byte2] ^= bitmask2; // Flip i-th bit
|
||||
|
||||
crc1 = ((uint32_t)aux[(bits/8)-3] << 16) |
|
||||
((uint32_t)aux[(bits/8)-2] << 8) |
|
||||
(uint32_t)aux[(bits/8)-1];
|
||||
crc2 = modesChecksum(aux,bits);
|
||||
if (0 == modesChecksum(aux, bits)) {
|
||||
// The error is fixed. Overwrite the original buffer with
|
||||
// the corrected sequence, and returns the error bit position
|
||||
msg[byte1] = aux[byte1];
|
||||
msg[byte2] = aux[byte2];
|
||||
|
||||
if (crc1 == crc2) {
|
||||
/* The error is fixed. Overwrite the original buffer with
|
||||
* the corrected sequence, and returns the error bit
|
||||
* position. */
|
||||
memcpy(msg,aux,bits/8);
|
||||
if (mm)
|
||||
{
|
||||
mm->crc = crc2;
|
||||
mm->iid = 0;
|
||||
mm->crcok = 1;
|
||||
}
|
||||
/* We return the two bits as a 16 bit integer by shifting
|
||||
* 'i' on the left. This is possible since 'i' will always
|
||||
* be non-zero because i starts from j+1. */
|
||||
return j | (i<<8);
|
||||
// We return the two bits as a 16 bit integer by shifting
|
||||
// 'i' on the left. This is possible since 'i' will always
|
||||
// be non-zero because i starts from j+1
|
||||
return (j | (i << 8));
|
||||
|
||||
aux[byte2] ^= bitmask2; /* Flip i-th bit back. */
|
||||
aux[byte2] ^= bitmask2; // Flip i-th bit back
|
||||
}
|
||||
|
||||
aux[byte1] ^= bitmask1; /* Flip j-th bit back. */
|
||||
aux[byte1] ^= bitmask1; // Flip j-th bit back
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
return (-1);
|
||||
}
|
||||
|
||||
/* Hash the ICAO address to index our cache of MODES_ICAO_CACHE_LEN
|
||||
|
@ -1260,61 +1241,6 @@ int ICAOAddressWasRecentlySeen(uint32_t addr) {
|
|||
|
||||
return a && a == addr && time(NULL)-t <= MODES_ICAO_CACHE_TTL;
|
||||
}
|
||||
|
||||
/* If the message type has the checksum xored with the ICAO address, try to
|
||||
* brute force it using a list of recently seen ICAO addresses.
|
||||
*
|
||||
* Do this in a brute-force fashion by xoring the predicted CRC with
|
||||
* the address XOR checksum field in the message. This will recover the
|
||||
* address: if we found it in our cache, we can assume the message is ok.
|
||||
*
|
||||
* This function expects mm->msgtype and mm->msgbits to be correctly
|
||||
* populated by the caller.
|
||||
*
|
||||
* On success the correct ICAO address is stored in the modesMessage
|
||||
* structure in the addr field.
|
||||
*
|
||||
* If the function successfully recovers a message with a correct checksum
|
||||
* it returns 1. Otherwise 0 is returned. */
|
||||
int bruteForceAP(unsigned char *msg, struct modesMessage *mm) {
|
||||
unsigned char aux[MODES_LONG_MSG_BYTES];
|
||||
int msgtype = mm->msgtype;
|
||||
int msgbits = mm->msgbits;
|
||||
|
||||
if (msgtype == 0 || /* Short air surveillance */
|
||||
msgtype == 4 || /* Surveillance, altitude reply */
|
||||
msgtype == 5 || /* Surveillance, identity reply */
|
||||
msgtype == 16 || /* Long Air-Air survillance */
|
||||
msgtype == 20 || /* Comm-A, altitude request */
|
||||
msgtype == 21 || /* Comm-A, identity request */
|
||||
msgtype == 24) /* Comm-C ELM */
|
||||
{
|
||||
uint32_t addr;
|
||||
uint32_t crc;
|
||||
int lastbyte = (msgbits/8)-1;
|
||||
|
||||
/* Work on a copy. */
|
||||
memcpy(aux,msg,msgbits/8);
|
||||
|
||||
/* Compute the CRC of the message and XOR it with the AP field
|
||||
* so that we recover the address, because:
|
||||
*
|
||||
* (ADDR xor CRC) xor CRC = ADDR. */
|
||||
crc = modesChecksum(aux,msgbits);
|
||||
aux[lastbyte] ^= crc & 0xff;
|
||||
aux[lastbyte-1] ^= (crc >> 8) & 0xff;
|
||||
aux[lastbyte-2] ^= (crc >> 16) & 0xff;
|
||||
|
||||
/* If the obtained address exists in our cache we consider
|
||||
* the message valid. */
|
||||
addr = aux[lastbyte] | (aux[lastbyte-1] << 8) | (aux[lastbyte-2] << 16);
|
||||
if (ICAOAddressWasRecentlySeen(addr)) {
|
||||
mm->addr = addr;
|
||||
return (1);
|
||||
}
|
||||
}
|
||||
return (0);
|
||||
}
|
||||
//
|
||||
// In the squawk (identity) field bits are interleaved as follows in
|
||||
// (message bit 20 to bit 32):
|
||||
|
@ -1452,39 +1378,29 @@ char *getMEDescription(int metype, int mesub) {
|
|||
mename = "Aircraft Operational Status Message";
|
||||
return mename;
|
||||
}
|
||||
|
||||
/* Decode a raw Mode S message demodulated as a stream of bytes by
|
||||
* detectModeS(), and split it into fields populating a modesMessage
|
||||
* structure. */
|
||||
//
|
||||
// Decode a raw Mode S message demodulated as a stream of bytes by detectModeS(),
|
||||
// and split it into fields populating a modesMessage structure.
|
||||
//
|
||||
void decodeModesMessage(struct modesMessage *mm, unsigned char *msg) {
|
||||
uint32_t crc2; /* Computed CRC, used to verify the message CRC. */
|
||||
char *ais_charset = "?ABCDEFGHIJKLMNOPQRSTUVWXYZ????? ???????????????0123456789??????";
|
||||
|
||||
/* Work on our local copy */
|
||||
// Work on our local copy
|
||||
memcpy(mm->msg, msg, MODES_LONG_MSG_BYTES);
|
||||
msg = mm->msg;
|
||||
|
||||
/* Get the message type ASAP as other operations depend on this */
|
||||
mm->msgtype = msg[0] >> 3; /* Downlink Format */
|
||||
mm->msgbits = modesMessageLenByType(mm->msgtype);
|
||||
// If we havn't already got it, get the message type ASAP as other operations depend on this
|
||||
if (mm->msgbits == 0) {
|
||||
mm->msgtype = msg[0] >> 3; // Downlink Format
|
||||
mm->msgbits = modesMessageLenByType(mm->msgtype);
|
||||
}
|
||||
|
||||
/* CRC is always the last three bytes. */
|
||||
mm->crc = ((uint32_t)msg[(mm->msgbits/8)-3] << 16) |
|
||||
((uint32_t)msg[(mm->msgbits/8)-2] << 8) |
|
||||
(uint32_t)msg[(mm->msgbits/8)-1];
|
||||
crc2 = modesChecksum(msg, mm->msgbits);
|
||||
mm->iid = (mm->crc ^ crc2);
|
||||
mm->errorbit = -1; // No errors fixed
|
||||
mm->phase_corrected = 0;
|
||||
mm->crc = modesChecksum(msg, mm->msgbits);
|
||||
|
||||
/* Check CRC and fix single bit errors using the CRC when
|
||||
* possible (DF 11 and 17). */
|
||||
mm->errorbit = -1; /* No error */
|
||||
if (mm->msgtype == 11)
|
||||
{mm->crcok = (mm->iid < 80);}
|
||||
else
|
||||
{mm->crcok = (mm->iid == 0);}
|
||||
|
||||
if (!mm->crcok && Modes.fix_errors && (mm->msgtype == 17)){
|
||||
// if (!mm->crcok && Modes.fix_errors && ((mm->msgtype == 11) || (mm->msgtype == 17))){
|
||||
if ((mm->crc) && (Modes.fix_errors) && (mm->msgtype == 17)) {
|
||||
// if ((mm->crc) && (Modes.fix_errors) && ((mm->msgtype == 11) || (mm->msgtype == 17))) {
|
||||
//
|
||||
// Fixing single bit errors in DF-11 is a bit dodgy because we have no way to
|
||||
// know for sure if the crc is supposed to be 0 or not - it could be any value
|
||||
|
@ -1495,51 +1411,56 @@ void decodeModesMessage(struct modesMessage *mm, unsigned char *msg) {
|
|||
// using the results. Perhaps check the ICAO against known aircraft, and check
|
||||
// IID against known good IID's. That's a TODO.
|
||||
//
|
||||
mm->errorbit = fixSingleBitErrors(msg, mm->msgbits, mm);
|
||||
mm->errorbit = fixSingleBitErrors(msg, mm->msgbits);
|
||||
if ((mm->errorbit == -1) && (Modes.aggressive)) {
|
||||
mm->errorbit = fixTwoBitsErrors(msg, mm->msgbits, mm);
|
||||
mm->errorbit = fixTwoBitsErrors(msg, mm->msgbits);
|
||||
}
|
||||
}
|
||||
//
|
||||
// Note that most of the other computation happens *after* we fix the
|
||||
// single/two bit errors, otherwise we would need to recompute the fields again.
|
||||
//
|
||||
if (mm->msgtype == 11) { // DF 11
|
||||
mm->crcok = (mm->crc < 80);
|
||||
mm->iid = mm->crc;
|
||||
mm->addr = (msg[1] << 16) | (msg[2] << 8) | (msg[3]);
|
||||
|
||||
/* Note that most of the other computation happens *after* we fix
|
||||
* the single bit errors, otherwise we would need to recompute the
|
||||
* fields again. */
|
||||
mm->ca = msg[0] & 7; /* Responder capabilities. */
|
||||
|
||||
// ICAO address
|
||||
mm->addr = (msg[1] << 16) | (msg[2] << 8) | (msg[3]);
|
||||
|
||||
/* DF 17 type (assuming this is a DF17, otherwise not used) */
|
||||
mm->metype = msg[4] >> 3; /* Extended squitter message type. */
|
||||
mm->mesub = msg[4] & 7; /* Extended squitter message subtype. */
|
||||
|
||||
/* Fields for DF4,5,20,21 */
|
||||
mm->fs = msg[0] & 7; /* Flight status for DF4,5,20,21 */
|
||||
mm->dr = msg[1] >> 3 & 31; /* Request extraction of downlink request. */
|
||||
mm->um = ((msg[1] & 7)<<3)| /* Request extraction of downlink request. */
|
||||
msg[2]>>5;
|
||||
|
||||
mm->modeA = decodeGillhamField((msg[2] << 8) | msg[3]);
|
||||
|
||||
/* DF 11 & 17: try to populate our ICAO addresses whitelist.
|
||||
* DFs with an AP field (xored addr and crc), try to decode it. */
|
||||
if (mm->msgtype != 11 && mm->msgtype != 17) {
|
||||
/* Check if we can check the checksum for the Downlink Formats where
|
||||
* the checksum is xored with the aircraft ICAO address. We try to
|
||||
* brute force it using a list of recently seen aircraft addresses. */
|
||||
if (bruteForceAP(msg,mm)) {
|
||||
/* We recovered the message, mark the checksum as valid. */
|
||||
mm->crcok = 1;
|
||||
} else {
|
||||
mm->crcok = 0;
|
||||
}
|
||||
} else {
|
||||
/* If this is DF 11 or DF 17 and the checksum was ok,
|
||||
* we can add this address to the list of recently seen
|
||||
* addresses. */
|
||||
if (mm->crcok && mm->errorbit == -1) {
|
||||
if (0 == mm->crc) {
|
||||
// DF 11 : if crc == 0 try to populate our ICAO addresses whitelist.
|
||||
addRecentlySeenICAOAddr(mm->addr);
|
||||
}
|
||||
|
||||
} else if (mm->msgtype == 17) { // DF 17
|
||||
mm->crcok = (mm->crc == 0);
|
||||
mm->addr = (msg[1] << 16) | (msg[2] << 8) | (msg[3]);
|
||||
|
||||
if (-1 != mm->errorbit) {
|
||||
// DF 17 : if (error corrected) force crc = 0 but do not try to add this address
|
||||
// to the whitelist of recently seen ICAO addresses.
|
||||
mm->crc = 0;
|
||||
|
||||
} else if (0 == mm->crc) {
|
||||
// DF 17 : if uncorrected and crc == 0 add this address to the whitelist of
|
||||
// recently seen ICAO addresses.
|
||||
addRecentlySeenICAOAddr(mm->addr);
|
||||
}
|
||||
|
||||
} else { // All other DF's
|
||||
// Compare the checksum with the whitelist of recently seen ICAO
|
||||
// addresses. If it matches one, then declare the message as valid
|
||||
mm->addr = mm->crc;
|
||||
mm->crcok = ICAOAddressWasRecentlySeen(mm->crc);
|
||||
}
|
||||
|
||||
// Fields for DF4,5,20,21
|
||||
mm->ca = // Responder capabilities
|
||||
mm->fs = msg[0] & 7; // Flight status for DF4,5,20,21
|
||||
mm->dr = (msg[1] >> 3) & 0x1F; // Request extraction of downlink request
|
||||
mm->um = ((msg[1] & 7) << 3) | (msg[2] >> 5); // Request extraction of downlink request
|
||||
|
||||
// Fields for DF5,21 = Gillham encoded Squawk
|
||||
if (mm->msgtype == 5 || mm->msgtype == 21) {
|
||||
mm->modeA = decodeGillhamField((msg[2] << 8) | msg[3]);
|
||||
}
|
||||
|
||||
// Fields for DF0, DF4, DF16, DF20 13 bit altitude
|
||||
|
@ -1548,9 +1469,12 @@ void decodeModesMessage(struct modesMessage *mm, unsigned char *msg) {
|
|||
mm->altitude = decodeAC13Field(((msg[2] << 8) | msg[3]), &mm->unit);
|
||||
}
|
||||
|
||||
/* Decode extended squitter specific stuff. */
|
||||
// Fields for DF17 squitter
|
||||
if (mm->msgtype == 17) {
|
||||
/* Decode the extended squitter message. */
|
||||
mm->metype = msg[4] >> 3; // Extended squitter message type
|
||||
mm->mesub = msg[4] & 7; // Extended squitter message subtype
|
||||
|
||||
// Decode the extended squitter message
|
||||
|
||||
if (mm->metype >= 1 && mm->metype <= 4) {
|
||||
/* Aircraft Identification and Category */
|
||||
|
@ -1612,7 +1536,6 @@ void decodeModesMessage(struct modesMessage *mm, unsigned char *msg) {
|
|||
}
|
||||
}
|
||||
}
|
||||
mm->phase_corrected = 0; /* Set to 1 by the caller if needed. */
|
||||
}
|
||||
//
|
||||
// This function gets a decoded Mode S Message and prints it on the screen
|
||||
|
@ -2082,7 +2005,9 @@ void detectModeS(uint16_t *m, uint32_t mlen) {
|
|||
mm.timestampMsg = Modes.timestampBlk + (j*6);
|
||||
sigStrength = (sigStrength + 0x7F) >> 8;
|
||||
mm.signalLevel = ((sigStrength < 255) ? sigStrength : 255);
|
||||
decodeModesMessage(&mm,msg);
|
||||
mm.msgbits = msglen;
|
||||
mm.msgtype = msg[0] >> 3;
|
||||
decodeModesMessage(&mm, msg);
|
||||
|
||||
/* Update statistics. */
|
||||
if (mm.crcok || use_correction) {
|
||||
|
@ -3070,6 +2995,7 @@ int decodeHexMessage(struct client *c) {
|
|||
// Non timemarked internet data has indeterminate delay
|
||||
mm.timestampMsg = -1;
|
||||
mm.signalLevel = -1;
|
||||
mm.msgbits = 0;
|
||||
|
||||
// Remove spaces on the left and on the right
|
||||
while(l && isspace(hex[l-1])) {
|
||||
|
|
Loading…
Reference in a new issue