mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
456 lines
18 KiB
Objective-C
456 lines
18 KiB
Objective-C
#import "TGPassportMRZ.h"
|
||
|
||
const NSUInteger TGPassportTD1Length = 30;
|
||
const NSUInteger TGPassportTD23Length = 44;
|
||
NSString *const TGPassportEmptyCharacter = @"<";
|
||
|
||
@implementation TGPassportMRZ
|
||
|
||
+ (instancetype)parseBarcodePayload:(NSString *)data
|
||
{
|
||
if (data.length == 0)
|
||
return nil;
|
||
|
||
if ([data rangeOfString:@"ANSI "].location != NSNotFound)
|
||
{
|
||
NSMutableDictionary *fields = [[NSMutableDictionary alloc] init];
|
||
[data enumerateLinesUsingBlock:^(NSString *line, BOOL *stop)
|
||
{
|
||
if (line.length < 4 || ![line hasPrefix:@"D"])
|
||
return;
|
||
|
||
NSString *field = [line substringToIndex:3];
|
||
NSString *value = [line substringFromIndex:3];
|
||
fields[field] = value;
|
||
}];
|
||
|
||
if (fields.count == 0)
|
||
return nil;
|
||
|
||
TGPassportMRZ *result = [[TGPassportMRZ alloc] init];
|
||
result->_documentType = @"DL";
|
||
|
||
NSString *issuingCountry = fields[@"DCG"];
|
||
result->_issuingCountry = issuingCountry;
|
||
|
||
result->_documentNumber = fields[@"DCF"];
|
||
|
||
NSString *firstName = fields[@"DAC"];
|
||
if (firstName == nil)
|
||
firstName = fields[@"DCT"];
|
||
result->_firstName = firstName;
|
||
|
||
NSString *lastName = fields[@"DCS"];
|
||
if (lastName == nil)
|
||
lastName = fields[@"DAB"];
|
||
result->_lastName = lastName;
|
||
|
||
NSString *middleName = fields[@"DAD"];
|
||
result->_middleName = middleName;
|
||
|
||
NSString *gender = fields[@"DBC"];
|
||
if ([gender isEqualToString:@"1"])
|
||
result->_gender = @"M";
|
||
else if ([gender isEqualToString:@"2"])
|
||
result->_gender = @"F";
|
||
|
||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
|
||
formatter.dateFormat = @"MMddyyyy";
|
||
|
||
NSDateFormatter *fallbackFormatter = [[NSDateFormatter alloc] init];
|
||
fallbackFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
|
||
fallbackFormatter.dateFormat = @"MMddyyyy";
|
||
|
||
NSString *expiryDate = fields[@"DBA"];
|
||
if (expiryDate.length > 0)
|
||
{
|
||
NSDate *date = [formatter dateFromString:expiryDate];
|
||
if (date == nil)
|
||
[fallbackFormatter dateFromString:expiryDate];
|
||
|
||
result->_expiryDate = date;
|
||
}
|
||
|
||
NSString *birthDate = fields[@"DBB"];
|
||
if (birthDate.length > 0)
|
||
{
|
||
NSDate *date = [formatter dateFromString:birthDate];
|
||
if (date == nil)
|
||
[fallbackFormatter dateFromString:birthDate];
|
||
|
||
result->_birthDate = date;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
else
|
||
{
|
||
NSCharacterSet *invertedBase64CharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="] invertedSet];
|
||
|
||
if ([data rangeOfCharacterFromSet:invertedBase64CharacterSet].location != NSNotFound)
|
||
return nil;
|
||
|
||
NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:data options:0];
|
||
if (decodedData.length == 0)
|
||
return nil;
|
||
|
||
NSString *decodedString = [[NSString alloc] initWithData:decodedData encoding:NSWindowsCP1251StringEncoding];
|
||
if (decodedString.length == 0)
|
||
return nil;
|
||
|
||
NSArray *components = [decodedString componentsSeparatedByString:@"|"];
|
||
if (components.count < 7)
|
||
return nil;
|
||
|
||
TGPassportMRZ *result = [[TGPassportMRZ alloc] init];
|
||
result->_documentType = @"DL";
|
||
result->_issuingCountry = @"RUS";
|
||
result->_nationality = @"RUS";
|
||
result->_documentNumber = components[0];
|
||
|
||
NSString *nativeFirstName = components[4];
|
||
result->_nativeFirstName = nativeFirstName;
|
||
result->_firstName = [self transliterateRussianName:nativeFirstName];
|
||
|
||
NSString *nativeLastName = components[3];
|
||
result->_nativeLastName = nativeLastName;
|
||
result->_lastName = [self transliterateRussianName:nativeLastName];
|
||
|
||
NSString *nativeMiddleName = components[5];
|
||
result->_nativeMiddleName = nativeMiddleName;
|
||
result->_middleName = [self transliterateRussianName:nativeMiddleName];
|
||
|
||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||
dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
|
||
[dateFormatter setDateFormat:@"yyyyMMdd"];
|
||
|
||
NSString *birthDate = components[6];
|
||
if (birthDate.length > 0)
|
||
result->_birthDate = [dateFormatter dateFromString:birthDate];
|
||
|
||
NSString *expiryDate = components[2];
|
||
if (expiryDate.length > 0)
|
||
result->_expiryDate = [dateFormatter dateFromString:expiryDate];
|
||
|
||
return result;
|
||
}
|
||
|
||
return nil;
|
||
}
|
||
|
||
+ (instancetype)parseLines:(NSArray<NSString *> *)lines
|
||
{
|
||
if (lines.count == 2)
|
||
{
|
||
if (lines[0].length != TGPassportTD23Length || lines[1].length != TGPassportTD23Length)
|
||
return nil;
|
||
|
||
TGPassportMRZ *result = [[TGPassportMRZ alloc] init];
|
||
result->_documentType = [lines[0] substringToIndex:1];
|
||
result->_documentSubtype = [self cleanString:[lines[0] substringWithRange:NSMakeRange(1, 1)]];
|
||
result->_issuingCountry = [self cleanString:[lines[0] substringWithRange:NSMakeRange(2, 3)]];
|
||
|
||
NSCharacterSet *emptyCharacterSet = [NSCharacterSet characterSetWithCharactersInString:TGPassportEmptyCharacter];
|
||
NSString *fullName = [[lines[0] substringWithRange:NSMakeRange(5, 39)] stringByTrimmingCharactersInSet:emptyCharacterSet];
|
||
NSArray *names = [fullName componentsSeparatedByString:@"<<"];
|
||
result->_lastName = [self nameString:names.firstObject];
|
||
result->_firstName = [self nameString:names.lastObject];
|
||
|
||
NSString *documentNumber = [self ensureNumberString:[lines[1] substringToIndex:9]];
|
||
NSInteger documentNumberCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(9, 1)]] integerValue];
|
||
if ([self isDataValid:documentNumber check:documentNumberCheck])
|
||
result->_documentNumber = documentNumber;
|
||
|
||
result->_nationality = [lines[1] substringWithRange:NSMakeRange(10, 3)];
|
||
NSString *birthDate = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(13, 6)]];
|
||
NSInteger birthDateCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(19, 1)]] integerValue];
|
||
if ([self isDataValid:birthDate check:birthDateCheck])
|
||
result->_birthDate = [self dateFromString:birthDate];
|
||
|
||
NSString *gender = [lines[1] substringWithRange:NSMakeRange(20, 1)];
|
||
if ([gender isEqualToString:TGPassportEmptyCharacter])
|
||
gender = nil;
|
||
result->_gender = gender;
|
||
|
||
NSString *expiryDate = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(21, 6)]];
|
||
NSInteger expiryDateCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(27, 1)]] integerValue];
|
||
if ([self isDataValid:expiryDate check:expiryDateCheck])
|
||
result->_expiryDate = [self dateFromString:expiryDate];
|
||
|
||
NSString *optional1 = [lines[1] substringWithRange:NSMakeRange(28, 14)];
|
||
NSString *optional1CheckString = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(42, 1)]];
|
||
NSInteger optional1CheckValue = [optional1CheckString isEqualToString:TGPassportEmptyCharacter] ? 0 : [optional1CheckString integerValue];
|
||
if ([self isDataValid:optional1 check:optional1CheckValue])
|
||
result->_optional1 = [self cleanString:optional1];
|
||
|
||
NSString *data = [NSString stringWithFormat:@"%@%d%@%d%@%d%@%@", documentNumber, (int)documentNumberCheck, birthDate, (int)birthDateCheck, expiryDate, (int)expiryDateCheck, optional1, optional1CheckString];
|
||
NSInteger dataCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(43, 1)]] integerValue];
|
||
if ([self isDataValid:data check:dataCheck])
|
||
{
|
||
if ([result->_documentType isEqualToString:@"P"] && [result->_documentSubtype isEqualToString:@"N"] && [result->_issuingCountry isEqualToString:@"RUS"])
|
||
{
|
||
NSString *lastName = [self transliterateRussianMRZString:result->_lastName];
|
||
result->_nativeLastName = lastName;
|
||
result->_lastName = [self transliterateRussianName:lastName];
|
||
NSString *firstName = [self transliterateRussianMRZString:result->_firstName];
|
||
result->_nativeFirstName = firstName;
|
||
result->_firstName = [self transliterateRussianName:firstName];
|
||
|
||
if (result->_documentNumber != nil)
|
||
{
|
||
NSString *lastSeriesDigit = [optional1 substringToIndex:1];
|
||
NSMutableString *fullDocumentNo = [[NSMutableString alloc] init];
|
||
[fullDocumentNo insertString:[result->_documentNumber substringToIndex:3] atIndex:0];
|
||
[fullDocumentNo appendString:lastSeriesDigit];
|
||
[fullDocumentNo appendString:[result->_documentNumber substringFromIndex:3]];
|
||
result->_documentNumber = fullDocumentNo;
|
||
}
|
||
}
|
||
|
||
|
||
result->_mrz = [lines componentsJoinedByString:@"\n"];
|
||
return result;
|
||
}
|
||
}
|
||
else if (lines.count == 3)
|
||
{
|
||
if (lines[0].length != TGPassportTD1Length || lines[1].length != TGPassportTD1Length || lines[2].length != TGPassportTD1Length)
|
||
return nil;
|
||
|
||
TGPassportMRZ *result = [[TGPassportMRZ alloc] init];
|
||
result->_documentType = [lines[0] substringToIndex:1];
|
||
result->_documentSubtype = [self cleanString:[lines[0] substringWithRange:NSMakeRange(1, 1)]];
|
||
result->_issuingCountry = [self cleanString:[lines[0] substringWithRange:NSMakeRange(2, 3)]];
|
||
|
||
NSString *documentNumber = [self ensureNumberString:[lines[0] substringWithRange:NSMakeRange(5, 9)]];
|
||
NSInteger documentNumberCheck = [[self ensureNumberString:[lines[0] substringWithRange:NSMakeRange(14, 1)]] integerValue];
|
||
if ([self isDataValid:documentNumber check:documentNumberCheck])
|
||
result->_documentNumber = documentNumber;
|
||
|
||
NSString *optional1 = [lines[0] substringWithRange:NSMakeRange(15, 15)];
|
||
result->_optional1 = [self cleanString:optional1];
|
||
|
||
NSString *birthDate = [self ensureNumberString:[lines[1] substringToIndex:6]];
|
||
NSInteger birthDateCheck = [[lines[1] substringWithRange:NSMakeRange(6, 1)] integerValue];
|
||
if ([self isDataValid:birthDate check:birthDateCheck])
|
||
result->_birthDate = [self dateFromString:birthDate];
|
||
|
||
NSString *gender = [lines[1] substringWithRange:NSMakeRange(7, 1)];
|
||
if ([gender isEqualToString:TGPassportEmptyCharacter])
|
||
gender = nil;
|
||
result->_gender = gender;
|
||
|
||
NSString *expiryDate = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(8, 6)]];
|
||
NSInteger expiryDateCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(14, 1)]] integerValue];
|
||
if ([self isDataValid:expiryDate check:expiryDateCheck])
|
||
result->_expiryDate = [self dateFromString:expiryDate];
|
||
|
||
result->_nationality = [lines[1] substringWithRange:NSMakeRange(15, 3)];
|
||
|
||
NSString *optional2 = [lines[1] substringWithRange:NSMakeRange(18, 11)];
|
||
result->_optional2 = optional2;
|
||
|
||
NSCharacterSet *emptyCharacterSet = [NSCharacterSet characterSetWithCharactersInString:TGPassportEmptyCharacter];
|
||
NSString *fullName = [self ensureAlphaString:lines[2]];
|
||
fullName = [fullName stringByTrimmingCharactersInSet:emptyCharacterSet];
|
||
NSArray *names = [fullName componentsSeparatedByString:@"<<"];
|
||
result->_lastName = [self nameString:names.firstObject];
|
||
result->_firstName = [self nameString:names.lastObject];
|
||
result->_mrz = [lines componentsJoinedByString:@"\n"];
|
||
|
||
return result;
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
+ (NSDateFormatter *)dateFormatter
|
||
{
|
||
static dispatch_once_t onceToken;
|
||
static NSDateFormatter *dateFormatter;
|
||
dispatch_once(&onceToken, ^
|
||
{
|
||
dateFormatter = [[NSDateFormatter alloc] init];
|
||
dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
|
||
dateFormatter.dateFormat = @"YYMMdd";
|
||
dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
||
});
|
||
return dateFormatter;
|
||
}
|
||
|
||
|
||
+ (NSDate *)dateFromString:(NSString *)string
|
||
{
|
||
return [[self dateFormatter] dateFromString:string];
|
||
}
|
||
|
||
+ (NSString *)cleanString:(NSString *)string
|
||
{
|
||
return [string stringByReplacingOccurrencesOfString:TGPassportEmptyCharacter withString:@""];
|
||
}
|
||
|
||
+ (NSString *)nameString:(NSString *)string
|
||
{
|
||
return [string stringByReplacingOccurrencesOfString:TGPassportEmptyCharacter withString:@" "];
|
||
}
|
||
|
||
+ (NSString *)ensureNumberString:(NSString *)string
|
||
{
|
||
return [[[[string stringByReplacingOccurrencesOfString:@"O" withString:@"0"] stringByReplacingOccurrencesOfString:@"U" withString:@"0"] stringByReplacingOccurrencesOfString:@"Q" withString:@"0"] stringByReplacingOccurrencesOfString:@"J" withString:@"0"];
|
||
}
|
||
|
||
+ (NSString *)ensureAlphaString:(NSString *)string
|
||
{
|
||
NSCharacterSet *validChars = [NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ<"];
|
||
NSCharacterSet *invalidChars = [validChars invertedSet];
|
||
|
||
string = [string stringByReplacingOccurrencesOfString:@"0" withString:@"O"];
|
||
return [self string:string byReplacingCharactersInSet:invalidChars withString:TGPassportEmptyCharacter];
|
||
}
|
||
|
||
+ (NSString *)string:(NSString *)string byReplacingCharactersInSet:(NSCharacterSet *)charSet withString:(NSString *)aString {
|
||
NSMutableString *s = [NSMutableString stringWithCapacity:string.length];
|
||
for (NSUInteger i = 0; i < string.length; ++i) {
|
||
unichar c = [string characterAtIndex:i];
|
||
if (![charSet characterIsMember:c]) {
|
||
[s appendFormat:@"%C", c];
|
||
} else {
|
||
[s appendString:aString];
|
||
}
|
||
}
|
||
return s;
|
||
}
|
||
|
||
+ (NSString *)transliterateRussianMRZString:(NSString *)string
|
||
{
|
||
NSDictionary *map = @
|
||
{
|
||
@"A": @"А",
|
||
@"B": @"Б",
|
||
@"V": @"В",
|
||
@"G": @"Г",
|
||
@"D": @"Д",
|
||
@"E": @"Е",
|
||
@"2": @"Ё",
|
||
@"J": @"Ж",
|
||
@"Z": @"З",
|
||
@"I": @"И",
|
||
@"Q": @"Й",
|
||
@"K": @"К",
|
||
@"L": @"Л",
|
||
@"M": @"М",
|
||
@"N": @"Н",
|
||
@"O": @"О",
|
||
@"P": @"П",
|
||
@"R": @"Р",
|
||
@"S": @"С",
|
||
@"T": @"Т",
|
||
@"U": @"У",
|
||
@"F": @"Ф",
|
||
@"H": @"Х",
|
||
@"C": @"Ц",
|
||
@"3": @"Ч",
|
||
@"4": @"Ш",
|
||
@"W": @"Щ",
|
||
@"X": @"Ъ",
|
||
@"Y": @"Ы",
|
||
@"9": @"Ь",
|
||
@"6": @"Э",
|
||
@"7": @"Ю",
|
||
@"8": @"Я",
|
||
@" ": @" "
|
||
};
|
||
|
||
NSMutableString *result = [[NSMutableString alloc] init];
|
||
[string enumerateSubstringsInRange:NSMakeRange(0, string.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, __unused BOOL *stop)
|
||
{
|
||
if (substring == nil)
|
||
return;
|
||
|
||
NSString *letter = map[substring];
|
||
if (letter != nil)
|
||
[result appendString:letter];
|
||
}];
|
||
return result;
|
||
}
|
||
|
||
+ (NSString *)transliterateRussianName:(NSString *)string
|
||
{
|
||
NSDictionary *map = @
|
||
{
|
||
@"А": @"A",
|
||
@"Б": @"B",
|
||
@"В": @"V",
|
||
@"Г": @"G",
|
||
@"Д": @"D",
|
||
@"Е": @"E",
|
||
@"Ё": @"E",
|
||
@"Ж": @"ZH",
|
||
@"З": @"Z",
|
||
@"И": @"I",
|
||
@"Й": @"I",
|
||
@"К": @"K",
|
||
@"Л": @"L",
|
||
@"М": @"M",
|
||
@"Н": @"N",
|
||
@"О": @"O",
|
||
@"П": @"P",
|
||
@"Р": @"R",
|
||
@"С": @"S",
|
||
@"Т": @"T",
|
||
@"У": @"U",
|
||
@"Ф": @"F",
|
||
@"Х": @"KH",
|
||
@"Ц": @"TS",
|
||
@"Ч": @"CH",
|
||
@"Ш": @"SH",
|
||
@"Щ": @"SHCH",
|
||
@"Ъ": @"IE",
|
||
@"Ы": @"Y",
|
||
@"Ь": @"",
|
||
@"Э": @"E",
|
||
@"Ю": @"IU",
|
||
@"Я": @"IA",
|
||
@" ": @" "
|
||
};
|
||
|
||
NSMutableString *result = [[NSMutableString alloc] init];
|
||
[string enumerateSubstringsInRange:NSMakeRange(0, string.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, __unused BOOL *stop)
|
||
{
|
||
if (substring == nil)
|
||
return;
|
||
|
||
NSString *letter = map[substring];
|
||
if (letter != nil)
|
||
[result appendString:letter];
|
||
}];
|
||
return result;
|
||
}
|
||
|
||
+ (bool)isDataValid:(NSString *)data check:(NSInteger)check
|
||
{
|
||
int32_t sum = 0;
|
||
uint8_t w[3] = { 7, 3, 1 };
|
||
|
||
for (NSUInteger i = 0; i < data.length; i++)
|
||
{
|
||
unichar c = [data characterAtIndex:i];
|
||
NSInteger d = 0;
|
||
if (c >= '0' && c <= '9')
|
||
d = c - '0';
|
||
else if (c >= 'A' && c <= 'Z')
|
||
d = (10 + c) - 'A';
|
||
else if (c != '<')
|
||
return false;
|
||
|
||
sum += d * w[i % 3];
|
||
}
|
||
|
||
if (sum % 10 != check)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
@end
|