Link to home
Start Free TrialLog in
Avatar of Rocco Galati
Rocco Galati

asked on

How to improve input data validation for a magnetic card reader in C/C++ and WxWidgets

I'm using WxWidgets and C/C++ under Ubuntu to write an application that use data coming from a magnetic card reader.
@sarabande gave me support to perform the string parsing and it works great!

Now, I would like to improve the code used to read the data since I realized that sometimes the users do not correctly swipe the card and the system is not able to process the string.

If the card is correctly swiped, the string is:

%URSMCC84R04E815LURSO  MARCO_ò8038001605011036724_

When the user fails to swipe the card, it usually produces a shorter string with strange characters or just a subset of the original string. For this reason, I'm trying to check the correct length of the string (it should be 53 characters) and if it's different, then the user gets an error.

It seems to work, but I would like to improve it.
Do you have suggestions, please?
If the swipe is not correct, the text  control should be cleared in order to accept a new string from the card reader (which acts like a keyboard).

void ProjectFrame::OntesseraText2(wxCommandEvent& event)
{
    wxString stringa_tessera = tessera->GetValue();
    // I read the string coming from the USB reader
    stringa_tessera.Length();
    int counter = static_cast<int>(stringa_tessera.Length());
     printf("Counter: %d\n", counter);
    if(counter > 52){

    wxString number = stringa_tessera.Mid(1, 16);  // Mid starts at offset 1 and takes 16 chars
    wxString rest = stringa_tessera.Mid(17);                         // consider the leading %
    surname = rest.BeforeFirst(wxUniChar(' '));   // now it should be "URSO"
    rest = rest.AfterFirst(wxUniChar(' '));
    name = rest.BeforeFirst(wxUniChar('_'));

    // %URSMCC84R04E815LURSO  MARCO_ò8038001605011036724_

    rest_ctrl->ChangeValue( rest ); // I load the string in the second TextCtrl just for test
    name_ctrl->ChangeValue( name );
    surname_ctrl->ChangeValue( surname );
    number_ctrl->ChangeValue( number );

//    wxStringTokenizer tokenizer(stringa_tessera, " ");
//    while ( tokenizer.HasMoreTokens() )
//{
//    wxString token = tokenizer.GetNextToken();
//    number_ctrl->ChangeValue( token );
//    // process token here
//}
        int num_char = static_cast<int>(stringa_tessera.Length());
        if (num_char > 40){
        Connection conn(false);
        conn.connect("isola", "localhost", "root", "isola");
        Query query = conn.query();


        /* Now SELECT */
        query << "SELECT * FROM Utenti";
        StoreQueryResult ares = query.store();
        wxString mystring;
      //  mystring << ares.num_rows();
      //  rest_ctrl->ChangeValue( mystring );
       // printf("numero: %d", ares.num_rows());
        for (size_t i = 0; i < ares.num_rows(); i++){
        if( (ares[i]["CF"]) == number.ToStdString()){
        // mystring << (const char *)ares[i]["CF"];
        rest_ctrl->ChangeValue("OK");
        secondpage = new SecondPage(NULL, 1);
        secondpage->user->SetLabel(name);
        secondpage->Refresh();
        secondpage->Update();
        secondpage->Show(true);
        this->Hide();
        break;
        }
        else {
        rest_ctrl->ChangeValue("NOT FOUND");
        identificazione->Show(TRUE);
        assistenza->Show(TRUE);
        tessera->Clear();
        }
	    }
        }

    }
    else {

        printf("Swiping not correct!\n");
    }

}

Open in new window


Thank you!
Avatar of noci
noci

1) DONT HARDCODE username & passwords in code
2) DONT publish code with passwords online

The data most probably has several checkable items (constraints) inside
String xyz  should start with....
Checksum is at position X and created by... (adding all ascii values?)
Column Y-Z hold a access value possible values are 'A', 'B', .. 'F'
You pull the name from the string, so the name should start at a certain place and NOT start with a blank?
If in a substring only letters ('A'.. 'Z') are allowed then verify all members of a substring are only letters in the valid set.

Those constraints give you your checks.
Avatar of Rocco Galati

ASKER

when the swiping is not correct, it can receive also garbage or not standard characters. Moreover, the string format is always the same, only the name and surname length can change.
since the name and surname can change in length, the control on the minimum number of characters I was doing is not correct.
If the other values are FIXED, then they should not change, ==> you can very that first pos == '%'and second == 'U'
etc. then you have point you can verify.
So in effect what is the meaning of: "%URSMCC84R04E815L"
And what is: "_ò8038001605011036724_"
what should be the distance between those (name?)  "URSO  MARCO"
There must be some anchors in the string you can find out and verify.
Starting from the standard string format:

%URSMCC84R04E815LURSO  MARCO_ò8038001605011036724_

Open in new window


We can say that:
-Strings always start with % and always ends with _
-The first 16 chars are always the user identifier. In this sample string it is URSMCC84R04E815L
-URSO is tha surname and MARCO is the name. This set of characters can vary depending on the user name
-After the name and the surname there is always a  termination character _
-The remaining characters (20) are always integers

By realizing that the name and the surname length can change, I cannot use the minimum length parameter to check if the swiping operation is OK or not.

I could only look for the control characters %, _, _, but I do not know how to do that.
I realized that I cannot only check if the string starts with % and ends with _ because I noticed that sometimes, during wrong swiping operations, you can get strings like "
%E_òE_

Open in new window

" which contains the control characters but they are not correct!
So in you can validate the name against a database where you can verify the identity code. (those should match).
There is an error in the the above string as the first char after _ is not a digit...
(disregarding wx* syntax for now) ...
Pseudo C like code:

string [0] == '%'
start_1 = &string[1];   // Identity part ...
start_2 = strtok(string,"_")    // start of 2nd part, the number string.....
start_3 = strtok(NULL,"_")    // start of 3rd part, should be emtpy string
if (1 + strlen(string_1) + 1 + strlen(string_2) + 1 + strlen(string_3)  != strlen(string))  {
   return ERROR_LENGTHS;
}
if (strlen(start_1) <  MIN_LEN_1) {
    return ERROR_SHORT_ID;
}
if (strlen(start_2) < MIN_LEN_2) {
   return ERROR_SHORT_NUM;
}
strncpy(id, start_1,  16)   / copy the first 16 chars..
id[16] = '\0'
strncpy(name, &start_1[16], 64);


search in a database for a match on id & name...

for (s = string_2, *s; s++) {
   if (!isdigit(*s))
      return ERROR_PART2_NONNUMERIC;
}
...

Open in new window

I have a database that I can use for verification, but we cannot think that all the users are already in the database.

I cannot throw an error if the string is in the correct format, but the user is not present in the database because this is a real situation that can really occur in some cases.

I would like to check only if the user is correctly swiped the card and if he did, then I can search in the database to check if the user exists, if not, the system should prompt "Ok, your swipe is OK, but you are not in the database so you cannot proceed".

I have to discriminate between "Wrong swipes" and "users who correctly swipe but they are not in the db"
You ave the sample data of valid cards, look at those to create the pattern that it must match.
Start with % as you noted [AZ0-9]+ [A-Z]+_[a-zA-Z0-9]+_ as well as the length

Not sure I understand the point, the card has to be authorized first. What you actually shoukd be validating, is the read if the card when authorizing it.
A poor scan will then reject the card whether at validation if you have it or through search if card is known and valid.


The consideration I consider is deals with whether hard coding validation in the software for a pattern that might change to allow more info. Point you need the number,
The problem is that the card reader works like an emulated keyboard so during the swipe, it inserts a character one at time.
So, if I try to check the length or to check for control characters, I will get an error till the string won't be completed.

I would like to find a way to wait until the string is entirely typed (when the swipe is finished) so I can run the controls on the entire string and not on partial string.

I do not know how to explain it better, unfortunately.

Since the card reader produces garbage characters when the swipe is not  correct, I cannot check if the string is entirely inserted because I do not have a control charatecter to check the string termination.
So you wait until you receive a %......
Then you wait a certain time (a valid swipe should take some time, say 1 second))
So all data received within 1 second after the % is your data....      that string can be validated.
Alternatively:
If there are 2 _  in the shown example after a % in a message then the bulk read is wait for %, read until _ and read until _...   With a 1 second timeout for the complete string.

Then parse the string., I gave you some rules you can use for validation... in the pseudo code earlier.. why is that not usable?
You can use  regular expression pattern matching like arnold proposes.
something like %([A-Z0-9]{16})([A-Z ]+)_([0-9]+)_    will give 3 regions, ID, Name, data..

So you need more examples... of both valid & invalid scans.
you are reading a character at a time, you only validate when you get all the data, i.e. wait a second or two before presuming that there is no more data coming..

seems your question is in conflict with your last statement.
I'm sorry if I'm not able to explain very well my problem, I'm just a beginner unfortunately.

I have a WxTextCtrl in WxWidgets with focus on so the cursor is already in the text control when the application starts.

Then, the user swipe the magnetic card and the text control is filled with a character at time and at the end of the swiping, I obtain a full string.
Now, if the user correctly swipe the card, I get this string:

%URSMCC84R04E815LURSO  MARCO_ò8038001605011036724_

Open in new window


otherwise, I can get something like:

òE_^E_

Open in new window

%E_òE_^E_

Open in new window

òE_

Open in new window

and so on..

How can I wait till the string is fully entered?

I saw the pseudo-code but its hard to me to implement it.
I tried something like:

 wxString stringa_tessera = tessera->GetValue();
    // I read the string coming from the USB reader

    wxString number = stringa_tessera.Mid(1, 16);  // Mid starts at offset 1 and takes 16 chars
    wxString rest = stringa_tessera.Mid(17);                         // consider the leading %
    surname = rest.BeforeFirst(wxUniChar(' '));   // now it should be "URSO"
    rest = rest.AfterFirst(wxUniChar(' '));
    name = rest.BeforeFirst(wxUniChar('_'));

    // %URSMCC84R04E815LURSO  MARCO_ò8038001605011036724_

    int counter_number = static_cast<int>(number.Length());
    int counter_name = static_cast<int>(name.Length());
    int counter_surname = static_cast<int>(surname.Length());
    int counter_rest = static_cast<int>(rest.Length());

    printf("CF: %d\n", counter_number);
    printf("name: %d\n", counter_name);
    printf("surname: %d\n", counter_surname);
    printf("rest: %d\n", counter_rest);

    int p = '%';
    int dash = '_';
 
  if ( (strchr((stringa_tessera.ToStdString().c_str()), p) != NULL) &&  (strchr((stringa_tessera.ToStdString().c_str()), p) != NULL) && (counter_number == 16) && (counter_name > 2) && (counter_surname > 2) ){

     printf("OK"); // identification OK
     identificazione->Hide();
     assistenza->Hide();
    }

    else if ((strchr((stringa_tessera.ToStdString().c_str()), p) == NULL) &&  (strchr((stringa_tessera.ToStdString().c_str()), p) == NULL) && counter_number > 1){
        printf("Error\n");
        identificazione->Show(TRUE); // show a static text with an error message
        assistenza->Show(TRUE);
    }

Open in new window

With that code, I get the error message even when the full string is correct because the if() condition works also when the string is not fully received yet. Then it prints "OK".

I would like to generate the error only when the string is received.
If you are starting the development, and it sounds as though in your current stage you are looking to receive data from the scan, which you have confirmed.

The next step after you Receive the scanned data is to extract the information/reference by which you store the card.
I.e. If you use a DB backend, at this point, you will be querying the DB for the provided card.
Your first validation would be whether there is a set of numbers, 13-16, if not, you reject to notify the user the card was not accepted.

As far as my new understanding, you've passed the read state.
I will try to explain it better, please look at this code:

     wxString stringa_tessera = tessera->GetValue();
    wxString number = stringa_tessera.Mid(1, 16);  // Mid starts at offset 1 and takes 16 chars
    wxString rest = stringa_tessera.Mid(17);                         // consider the leading %
    surname = rest.BeforeFirst(wxUniChar(' '));   // now it should be "URSO"
    rest = rest.AfterFirst(wxUniChar(' '));
    name = rest.BeforeFirst(wxUniChar('_'));

        int num_char = static_cast<int>(stringa_tessera.Length());
        if (num_char > 40){
        Connection conn(false);
        conn.connect("isola", "localhost", "root", "isola");
        Query query = conn.query();

        /* Now SELECT */
        query << "SELECT * FROM Utenti";
        StoreQueryResult ares = query.store();
        wxString mystring;
      //  mystring << ares.num_rows();
      //  rest_ctrl->ChangeValue( mystring );
       // printf("numero: %d", ares.num_rows());
        for (size_t i = 0; i < ares.num_rows(); i++){
        if( (ares[i]["CF"]) == number.ToStdString()){
        // mystring << (const char *)ares[i]["CF"];
        rest_ctrl->ChangeValue("OK");
        secondpage = new SecondPage(NULL, 1);
        secondpage->user->SetLabel(name);
        secondpage->Refresh();
        secondpage->Update();
        secondpage->Show(true);
        this->Hide();
        break;
        }
        else {
        rest_ctrl->ChangeValue("NOT FOUND");
        identificazione->Show(TRUE);
        assistenza->Show(TRUE);
        tessera->Clear();
        }
	    }
        }

Open in new window


This code load into stringa_tessera everything is included in the WxTextCtrl and try to parse it in order to obtain the 16-char code, the name and the surname.
At this point, I do this:

1. check if the length is greater than 40
    1.1. if yes, it check if the user exists in the database
    1.2. if the user exists, than load the next frame
2. If not, it generates the error

Since the characters are filled one at time, the error (point 2) is always generated till the string in not fully entered.
So I always get the ERROR message until the string is not fully entered (swipe is not complete).

What can I do to display the error message only after all the characters have been typed?

When a user swipe, stringa_tessera is made in this way:
%URSMCC84R04E815LURSO  MARCO_ò8038001605011036724_
stringa_tessera=""
stringa_tessera="%"
stringa_tessera="%U"
stringa_tessera="%UR"
stringa_tessera="%"URS
stringa_tessera="%URSM"
...
...
stringa_tessera="%URSMCC84R04E815LUR
...
stringa_tessera="%URSMCC84R04E815LURSO  MARCO_ò8038001605011036724_"

Open in new window

The error routine is important because I need to clear the TextForm when the swipe is not correct otherwise the correct string will be added to the previous content which was not correct and the string parsing won't work.
Ok  so you do NOTHING on asserting the content until you see:
^%(.*)_(.*)_$               [Using regular expression sytax SHOULD make discussing this much easier]  see also  https://regexr.com/
^= start of string, $ = end of string
() = pickup a value aka field value
. = match any character
* match 0 ( {0,} or more, + = match 1 or more {1,}   in general more {n} specified length {n,m} = beteen n and m length
[A-Z]  = match a letter character capital only..
[0-9] = match numeric position
[0-9A-Z ] = match letter, digit or space
Anything else match literal...


Call an evaluation funtion:
test_my_string(string_tessera)

^%([0-9A-Z]{16})([A-Z]+) +([A-Z]+)_ò([0-9]+)_$

Will match your string IF
start with %
field 1 (id ) = 16 long,
there is a lastname (at least one character)
and a firstname (at least one character)
one: _
one:  ò
a collection of digits
ends with _.
No match on this should return INCOMPLETE...

Copy the fields to a structure: stringval. (fields: id, surname, name, data).
You can calculate the length of the ID field (1 = 16)
You can calculate the lenght of the  Surname field: (1 .. N) you define acceptable length
You can calculate the length of the Name field (1.. N)  you define acceptable length
You can calculate the length of the last, data field (1..N) you define acceptable length.
THen query the database: SELECT count(*) FROM database db WHERE db.id == stringa.id AND db.surname = stringa.surname AND  db.name == stringa.name)  where the result should be == 1
any other test???....
If  any of the above tests fail the  test_my_string() function should return FAILED
now you have validated the string and you can return SUCCESS.


You can mark the time when a % is typed.
If the string does NOT get stable [ doesn't grow ] in 2 seconds (or the time it takes to read the string)    then signal FAILURE.
you can use alarm() to get an timer interrupt if needed.
@Noci: thank you for all your support and explaination!

Your suggested algorithm should work great and surely does what I need to solve the problem.
Unfortunately, it is too much complex for me and I can't implement it by myself.

Should it be something like:

string a = stringa_tessera.ToStdString();
  
      regex b("^%([0-9A-Z]{16})([A-Z]+) +([A-Z]+)_ò([0-9]+)_$");   
  
    if ( regex_match(a, b) ) 
        cout << "OK";
     else
            cout << "ERROR"; 

Open in new window


this code?

The problem is that it will generate error after the second character because the string is not complete yet but the if() condition will be executed anyway.
You have to do a while loop to read in the entire data stream from the card reader which you append into a string variable.
you can not evaluate the string one byte at a time. you could look for the first % to proceed, though as your example points out that when the scan is incorrect, there is no % seen, so you will end up in an endless loop without any input to the user that they did anything wrong.
The point being, user swipes card, they either get an OK, or they get an error swipe card again, no such card, invalid card, etc.

Unfortunately, I am unfamiliar with what you are working with.
The example is to get you on the way on how to interact with the card.

Unfortunately, I am uncertain.

usually, you have a loop that you are in, looking for card input, when card input is seen you send it for validation either through a pattern match as noci and I referenced, and the length of the string to reduce the processing cost of checking with a DB when you do not have a valid data.
the problem is that I can't know when the string is complete in order to start and process it.

what about a timer which starts when the first character is received?

do you know how to implement it with code?
It looks like the handler for time is part of the API you are using, tessera->GetValue();
usually you can use timeouts, alarms to handle , sginals, https://cboard.cprogramming.com/linux-programming/148316-using-alarm-other-signals.html
event handlers to ...

you are using an API, and whatever is returned from tesera->GetrValue(); is presumed to be what was scanned.

I too am interested in details, but when dealing with an API, you have to rely on the definition.
i.e. to get data from the scanner, use tesera->getvalue() and it will return an array of characters, or a string.
Ok here is a start.... looks a bit like your code, but it should give an idea..
In stead of just sending "OK" you may implement further checks to validate /cross reference data

   string a = stringa_tessera.ToStdString();
   starttime= read_wallclock();
  while (read_wall_clock() - starttime < 3 seconds) {
      string a = stringa_tessera.ToStdString();
      regex b("^%([0-9A-Z]{16})([A-Z]+) +([A-Z]+)_ò([0-9]+)_$");   
  
    if ( regex_match(a, b) ) 
        cout << "OK";
 
   }
 clear_input_buffer();
   cout <<"ERROR"

Open in new window

This question needs an answer!
Become an EE member today
7 DAY FREE TRIAL
Members can start a 7-Day Free trial then enjoy unlimited access to the platform.
View membership options
or
Learn why we charge membership fees
We get it - no one likes a content blocker. Take one extra minute and find out why we block content.