Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 94 additions & 3 deletions passport/dg1_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,100 @@ type Passport struct {
func ParseDG1(data string) (*Passport, error) {
dg1Raw, err := hex.DecodeString(data)
if err != nil {
return nil, fmt.Errorf("invalid TD3 format: data should be a hexadecimal string: %w", err)
return nil, fmt.Errorf("failed to decode DG1 data from hex: %w", err)
}
dg1RawWithoutTag := dg1Raw[dg1TagSize:]

//nolint:gocritic // We need len from UTF8 string
dg1RawLen := len(string(dg1Raw))
Comment thread
ilya-korotya marked this conversation as resolved.
Outdated

// compare length with tag
switch dg1RawLen {
case 95:
return ParseTD1(dg1Raw)
case 93:
return ParseTD3(dg1Raw)
}

return nil, fmt.Errorf(
"invalid DG1 format: data should be either 95 (TD1) or 93 (TD3) characters long: %d",
dg1RawLen)
}
Comment thread
ilya-korotya marked this conversation as resolved.

// ParseTD1 parses the provided DG1 data in TD1 format and returns a Passport struct.
// TD1 format consists of 3 lines of 30 characters each (total 90 characters).
func ParseTD1(data []byte) (*Passport, error) {
dg1RawWithoutTag := data[dg1TagSize:]

dg1 := string(dg1RawWithoutTag)
if len(dg1) != 90 {
return nil, fmt.Errorf(
"invalid TD1 format: data should be 90 characters long: %d",
len(dg1),
)
}

line1 := dg1[:30]
line2 := dg1[30:60]
line3 := dg1[60:90]

// Parse Line 1: Document code (2), Issuing state (3), Document number (9),
// Check digit (1), Optional data (15)
documentType := trimPlaceholder(line1[:2])
issuingCountry := trimPlaceholder(line1[2:5])
documentNumber := trimPlaceholder(line1[5:14])
checkDigitNumber := trimPlaceholder(line1[14:15])

// Parse Line 2: Date of birth (6), Check digit (1), Sex (1), Date of expiry (6),
// Check digit (1), Nationality (3), Optional data (11), Composite check digit (1)
dateOfBirth := trimPlaceholder(line2[:6])
checkDigitDOB := trimPlaceholder(line2[6:7])
sexChar := line2[7:8]
dateOfExpiry := trimPlaceholder(line2[8:14])
checkDigitExpiry := trimPlaceholder(line2[14:15])
nationality := trimPlaceholder(line2[15:18])
checkDigitFinal := trimPlaceholder(line2[29:30])

// Parse Line 3: Name of holder (30)
holderName := parseHolderName(line3)

// Determine the sex value
var sexValue Sex
switch sexChar {
case "M":
sexValue = Male
case "F":
sexValue = Female
case "X":
sexValue = Other
default:
sexValue = Other
}

// Combine optional data fields

Comment thread
ilya-korotya marked this conversation as resolved.
Outdated
passport := &Passport{
DocumentType: documentType,
IssuingCountry: issuingCountry,
DocumentNumber: documentNumber,
HolderName: holderName,
Nationality: nationality,
DateOfBirth: dateOfBirth,
Sex: sexValue,
DateOfExpiry: dateOfExpiry,
PersonalNumber: "",
CheckDigitNumber: checkDigitNumber,
CheckDigitDOB: checkDigitDOB,
CheckDigitExpiry: checkDigitExpiry,
CheckDigitPersonal: "", // TD1 doesn't have a separate personal number check digit
CheckDigitFinal: checkDigitFinal,
Raw: data,
}

return passport, nil
}

func ParseTD3(data []byte) (*Passport, error) {
dg1RawWithoutTag := data[dg1TagSize:]

dg1 := string(dg1RawWithoutTag)
if len(dg1) != 88 {
Expand Down Expand Up @@ -92,7 +183,7 @@ func ParseDG1(data string) (*Passport, error) {
PersonalNumber: strings.TrimSpace(line2[28:42]), // 14 bytes
CheckDigitPersonal: trimPlaceholder(line2[42:43]), // 1 byte
CheckDigitFinal: trimPlaceholder(line2[43:44]), // 1 byte
Raw: dg1Raw,
Raw: data,
}

return passport, nil
Expand Down
257 changes: 253 additions & 4 deletions passport/dg1_parser_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package passport

import (
"encoding/hex"
"testing"

"github.com/stretchr/testify/require"
)

// DG1 is the same as the MRZ data, but with a group tag at the beginning.
func mrzToDg1(mrz string) string {
func mrzToDg1_TD3(mrz string) []byte {
tag := []byte{97, 91, 95, 31, 88}
mrzBytes := []byte(mrz)
b := append(tag, mrzBytes...)
return hex.EncodeToString(b)
return b
}

func mrzToDg1_TD1(mrz string) []byte {
tag := []byte{97, 93, 95, 31, 90}
mrzBytes := []byte(mrz)
b := append(tag, mrzBytes...)
return b
}

func TestParseDG1(t *testing.T) {
Comment thread
ilya-korotya marked this conversation as resolved.
Outdated
Expand Down Expand Up @@ -68,7 +74,7 @@ func TestParseDG1(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseDG1(mrzToDg1(tt.input))
result, err := ParseTD3(mrzToDg1_TD3(tt.input))
require.NoError(t, err)

// Check each field individually for better error messages
Expand All @@ -93,3 +99,246 @@ func TestParseDG1(t *testing.T) {
})
}
}

func TestParseDG1_TD1(t *testing.T) {
tests := []struct {
name string
input string
expected *Passport
shouldError bool
}{
{
name: "Valid TD1 document - Common id card",
input: "IDUKRAC12345671<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4KUZNETSOV<<VALERIY<<<<<<<<<<<<",
expected: &Passport{
DocumentType: "ID",
IssuingCountry: "UKR",
DocumentNumber: "AC1234567",
HolderName: "KUZNETSOV VALERIY",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Short document type",
input: "I<UKRAC12345671<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4KUZNETSOV<<VALERIY<<<<<<<<<<<<",
expected: &Passport{
DocumentType: "I",
IssuingCountry: "UKR",
DocumentNumber: "AC1234567",
HolderName: "KUZNETSOV VALERIY",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Shord document number",
Comment thread
ilya-korotya marked this conversation as resolved.
Outdated
input: "IDUKRAC12345<<1<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4KUZNETSOV<<VALERIY<<<<<<<<<<<<",
expected: &Passport{
DocumentType: "ID",
IssuingCountry: "UKR",
DocumentNumber: "AC12345",
HolderName: "KUZNETSOV VALERIY",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Optional identifier present",
input: "IDUKRAC12345<<112345<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4KUZNETSOV<<VALERIY<<<<<<<<<<<<",
expected: &Passport{
DocumentType: "ID",
IssuingCountry: "UKR",
DocumentNumber: "AC12345",
HolderName: "KUZNETSOV VALERIY",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Double given name",
input: "IDUKRAC12345671<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4KUZNETSOV<<VALERIY<VICTOR<<<<<",
expected: &Passport{
DocumentType: "ID",
IssuingCountry: "UKR",
DocumentNumber: "AC1234567",
HolderName: "KUZNETSOV VALERIY VICTOR",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Long name: triple given name",
input: "IDUKRAC12345671<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4LUCA<<MATTEO<ROSSI<BIAN<<<<<<<",
expected: &Passport{
DocumentType: "ID",
IssuingCountry: "UKR",
DocumentNumber: "AC1234567",
HolderName: "LUCA MATTEO ROSSI BIAN",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Long name: double surname and given name",
input: "IDUKRAC12345671<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4LUCA<MATTEO<<ROSSI<BIAN<<<<<<<",
expected: &Passport{
DocumentType: "ID",
IssuingCountry: "UKR",
DocumentNumber: "AC1234567",
HolderName: "LUCA MATTEO ROSSI BIAN",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Different nationality and issuing country",
input: "IDESPAC12345671<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4LUCA<MATTEO<<ROSSI<BIAN<<<<<<<",
expected: &Passport{
DocumentType: "ID",
IssuingCountry: "ESP",
DocumentNumber: "AC1234567",
HolderName: "LUCA MATTEO ROSSI BIAN",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Only name",
input: "I<UKRAC12345671<<<<<<<<<<<<<<<9603092M3508033UKR<<<<<<<<<<<4ONLYNAME<<<<<<<<<<<<<<<<<<<<<<",
expected: &Passport{
DocumentType: "I",
IssuingCountry: "UKR",
DocumentNumber: "AC1234567",
HolderName: "ONLYNAME",
Nationality: "UKR",
DateOfBirth: "960309",
Sex: Male,
DateOfExpiry: "350803",
CheckDigitNumber: "1",
CheckDigitDOB: "2",
CheckDigitExpiry: "3",
CheckDigitFinal: "4",
},
},
{
name: "Valid TD1 document - Minimal values",
input: "I<<<<<<<<<<<<<0<<<<<<<<<<<<<<<0000000M0000000<<<<<<<<<<<<<<0<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<",
expected: &Passport{
DocumentType: "I",
IssuingCountry: "",
DocumentNumber: "",
DateOfBirth: "000000",
Sex: Male,
DateOfExpiry: "000000",
CheckDigitNumber: "0",
CheckDigitDOB: "0",
CheckDigitExpiry: "0",
CheckDigitFinal: "0",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseTD1(mrzToDg1_TD1(tt.input))
require.NoError(t, err)
// Check each field individually for better error messages
require.Equal(t, tt.expected.DocumentType, result.DocumentType, "DocumentType mismatch")
require.Equal(
t,
tt.expected.IssuingCountry,
result.IssuingCountry,
"IssuingCountry mismatch",
)
require.Equal(
t,
tt.expected.DocumentNumber,
result.DocumentNumber,
"DocumentNumber mismatch",
)
require.Equal(t, tt.expected.DateOfBirth, result.DateOfBirth, "DateOfBirth mismatch")
require.Equal(t, tt.expected.Sex, result.Sex, "Sex mismatch")
require.Equal(t, tt.expected.DateOfExpiry, result.DateOfExpiry, "DateOfExpiry mismatch")
require.Equal(t, tt.expected.Nationality, result.Nationality, "Nationality mismatch")
require.Equal(t, tt.expected.HolderName, result.HolderName, "HolderName mismatch")

// check check sum digits
require.Equal(
t,
tt.expected.CheckDigitNumber,
result.CheckDigitNumber,
"CheckDigitNumber mismatch",
)
require.Equal(
t,
tt.expected.CheckDigitDOB,
result.CheckDigitDOB,
"CheckDigitDOB mismatch",
)
require.Equal(
t,
tt.expected.CheckDigitExpiry,
result.CheckDigitExpiry,
"CheckDigitExpiry mismatch",
)
require.Equal(
t,
tt.expected.CheckDigitFinal,
result.CheckDigitFinal,
"CheckDigitFinal mismatch",
)

// Verify raw data is present
require.NotEmpty(t, result.Raw, "Raw data should not be empty")
})
}
}
Loading