RioMd5 Class Reference

#include <RioExplorer.h>

Public Member Functions

bool getMd5Sum (char *, char *, bool=false)
 getMd5sum obtém o md5 checksum do arquivo objectName e armazena em md5sum; armazena NULL caso objectName seja um diretório ou a operação falhe; retorna true se a operação for realizada com sucesso, e false caso contrário.
bool updateCache ()
 This method ensures that the cache, if existant, is fully updated.
void copyCache (RioMd5 *)
 copyCache makes md5Cache equal as srcMd5.md5Cache, if this instance of RioMd5 was created with cache; used to load cache from another cache
bool importData (QString, CRioSession *)
 importData gets data from all files in srcDirectory and stores on md5Cache if RioMd5 was created with cache; used to load cache from remote md5 data
 RioMd5 (RioExplorer *, CRioSession *)
 RioMd5 is the class that is responsible for returning the md5 checksums for both local and remote files.
 RioMd5 (RioExplorer *, CRioSession *, char *)
 ~RioMd5 ()

Private Member Functions

char * calculateMD5 (const char *, char *)
void loadCache (QString, vector< QDomElement > &)
 loadCache carrega todos os itens armazenados na cache do diretório src e de todos os seus subdiretórios no vetor md5Cache.

Private Attributes

RioExplorerparent
CRioSessionmd5Session
QString srcDirectory
vector< QDomElement > md5Cache
QString MD5CACHEFILE

Detailed Description

Definition at line 228 of file RioExplorer.h.


Constructor & Destructor Documentation

RioMd5::RioMd5 ( RioExplorer explorer,
CRioSession session 
)

RioMd5 is the class that is responsible for returning the md5 checksums for both local and remote files.

It may or may not load previously calculated checksums from am XML cache located at a local directory. //continue

Definition at line 3287 of file RioExplorer.cpp.

03288 {
03289     parent = explorer;
03290     md5Session = session;
03291 }

RioMd5::RioMd5 ( RioExplorer explorer,
CRioSession session,
char *  src 
)

Definition at line 3293 of file RioExplorer.cpp.

03294 {
03295     parent = explorer;
03296     md5Session = session;
03297     srcDirectory = QString( src );
03298     MD5CACHEFILE = explorer->md5CacheFile();
03299     loadCache( srcDirectory , md5Cache );
03300 }

RioMd5::~RioMd5 (  ) 

Definition at line 3302 of file RioExplorer.cpp.

03303 {
03304     if( srcDirectory.isNull() ) return; //RioMd5 created without loading cache
03305 
03306     #ifdef RIO_DEBUG2
03307     RioErr << "Destroying cache for " << srcDirectory.latin1() << endl;
03308     #endif
03309     //this element will store directory elements; each of these elements will
03310     //be children of the root elememnt and will contain the tag elements that
03311     //will be stored to the XML files on the respective directory
03312     QDomDocument doc( "document" );
03313     QDomElement root = doc.createElement( "root" );
03314     doc.appendChild( root );
03315 
03316     vector<QDomElement>::iterator elemIt;
03317     for( elemIt = md5Cache.begin(); elemIt != md5Cache.end(); elemIt++ )
03318     {
03319         //getting directory
03320         QString dir = (*elemIt).attribute( "name" ).copy();
03321         if( dir.findRev( '/' ) == -1 )
03322             dir = "";
03323         else
03324             dir.truncate( dir.findRev( '/' ) ); //getting directory
03325 
03326         //getting name
03327         QString newName = (*elemIt).attribute( "name" );
03328         if( dir != "" )
03329             newName.remove( 0, dir.length() + 1 );
03330         (*elemIt).setAttribute( "name", newName );
03331 
03332         //checking if the directory has already being added to the document
03333         bool found = false;
03334         QDomNode node = root.firstChild();
03335         while( !node.isNull() && !found )
03336         {
03337             if( node.isElement() )
03338             {
03339                 QDomElement elem = node.toElement();
03340                 if( ( elem.attribute( "dir" ) == dir ) ) //case positive
03341                 {
03342                     elem.appendChild( *elemIt );
03343                     found = true;
03344                 }
03345             }
03346             node = node.nextSibling();
03347         }
03348         if( !found ) //case negative
03349         {
03350             QDomElement newDir = doc.createElement( "root" );
03351             newDir.setAttribute( "dir", dir );
03352             newDir.appendChild( *elemIt ); //adding tag to directory
03353             root.appendChild( newDir ); //adding directory to main document
03354         }
03355     }
03356 
03357     QDomNode exportNode = root.firstChild();
03358     while( !exportNode.isNull() )
03359     {
03360         //constructing XML file path
03361         QString fileOutput = exportNode.toElement().attribute( "dir" ).copy();
03362         if( fileOutput == "" )
03363             fileOutput = srcDirectory;
03364         else
03365             fileOutput = srcDirectory + '/' + fileOutput;
03366         fileOutput += '/';
03367         fileOutput += MD5CACHEFILE;
03368 
03369         #ifdef RIO_DEBUG2
03370         RioErr << "Exporting node to file: " << fileOutput.latin1() << endl; 
03371         #endif
03372 
03373         //root element can't contain attributes
03374         exportNode.toElement().removeAttribute( "dir" );
03375         QFile file( fileOutput );
03376         if( file.exists() && !file.remove() )
03377         {
03378             #ifdef RIO_DEBUG2
03379             RioErr << "Couldn't reset file " << fileOutput.latin1() << endl;
03380             #endif
03381 
03382             exportNode = exportNode.nextSibling();
03383             continue;
03384         }
03385         file.open( IO_ReadWrite );
03386         QTextStream stream( &file );
03387         exportNode.save( stream, 0 );
03388 
03389         exportNode = exportNode.nextSibling();
03390     }
03391 }


Member Function Documentation

char * RioMd5::calculateMD5 ( const char *  filename,
char *  buf 
) [private]

Definition at line 3835 of file RioExplorer.cpp.

03836 {
03837     MD5_CTX state;
03838 
03839     // Tamanho máximo do arquivo a ser lido
03840     unsigned int size = UINT_MAX;
03841     unsigned char digest[16];
03842     char hex[17] = "0123456789abcdef";
03843     char temp[100];
03844     char *m;
03845     int fd;
03846     int di = 0;
03847     int rc;
03848     struct stat st;
03849 
03850     fd = open( filename, O_RDONLY );
03851     MD5_Init( &state );
03852 
03853     if( fstat( fd, &st ) == -1 ) 
03854     {
03855         close( fd );
03856         if( buf )
03857             strcpy( buf, "0" );
03858         else
03859             buf = strdup( "0" );
03860         return buf;
03861     }
03862 
03863     if( (unsigned int) st.st_size < size )
03864         size = st.st_size;
03865 
03866     if( ( m = (char *) mmap(
03867              (void *) 0, size, PROT_READ, MAP_PRIVATE, fd, 0 ) ) != MAP_FAILED ) 
03868     {
03869         MD5_Update( &state, (unsigned char *) m, size );
03870         MD5_Final( digest, &state );
03871         munmap( m, size );
03872 
03873         for ( di = 0, rc = 0; di < 16; ++di, rc += 2 ) 
03874         {
03875             temp[ rc ] = hex[ digest[di] >> 4 ];
03876             temp[ rc + 1 ] = hex[ digest[di] & 15 ];
03877         }
03878         temp[ 32 ] = 0;
03879         sprintf( temp, "%s", temp );
03880 
03881         if( buf == NULL )
03882             buf = strdup( temp );
03883         else
03884             strcpy( buf, temp );
03885     }
03886     else
03887     {
03888         if( buf )
03889             strcpy( buf, "0" );
03890         else
03891             buf = strdup( "0" );
03892         return buf;
03893     }
03894     close( fd );
03895     return buf;
03896 }

void RioMd5::copyCache ( RioMd5 srcMd5  ) 

copyCache makes md5Cache equal as srcMd5.md5Cache, if this instance of RioMd5 was created with cache; used to load cache from another cache

Definition at line 3721 of file RioExplorer.cpp.

03722 {
03723     if( !srcDirectory.isNull() ) //cache created
03724     {
03725         md5Cache.clear();
03726 
03727         vector<QDomElement>::iterator it;
03728         for( it = srcMd5->md5Cache.begin(); it != srcMd5->md5Cache.end(); it++ )
03729         {
03730             QDomElement newElement = it->cloneNode().toElement();
03731             md5Cache.push_back( newElement );
03732         }
03733     }
03734 }

bool RioMd5::getMd5Sum ( char *  objectName,
char *  md5sum,
bool  notifyError = false 
)

getMd5sum obtém o md5 checksum do arquivo objectName e armazena em md5sum; armazena NULL caso objectName seja um diretório ou a operação falhe; retorna true se a operação for realizada com sucesso, e false caso contrário.

O checksum é obtido, para arquivos locais, através de cálculo ou, se possível, busca na cache (que já está carregada), e para arquivos remotos, solicitando ao servidor. O parâmetro booleano notifyError (default true) indica se getObjectMd5Sum deve ou não imprimir erro caso objectName não exista. Se a cache for utilizada, ela é atualizada conforme seja necessário.

Definition at line 3463 of file RioExplorer.cpp.

03464 {
03465     bool found = false;
03466 
03467     QString name( objectName );
03468     // MD5Sum cannot be calculated if the cache is used, but the searched file
03469     // is outside the scope of the source directory of the cache
03470     if( !srcDirectory.isNull() &&
03471         name.left( srcDirectory.length() ) != srcDirectory )
03472     {
03473         md5sum = NULL;
03474         return false;
03475     }
03476 
03477     if( !parent->checkSession( md5Session ) )
03478     {
03479         md5sum = NULL;
03480         return false;
03481     }
03482     ObjectInfo searchInfo;
03483     if (!parent->getObjectInfo( (char *)objectName, md5Session, &searchInfo,
03484                                 notifyError ))
03485     {
03486         md5sum = NULL;
03487         return false;
03488     }
03489 
03490     #ifdef RIO_DEBUG2
03491     RioErr << "getMd5Sum for " << objectName << endl;
03492     #endif
03493 
03494     if( md5Session == NULL ) //LOCAL object
03495     {
03496         if( !srcDirectory.isEmpty() ) //cache is used
03497         {
03498             //consistency check information: size and modification time
03499             RioObjectSize size = searchInfo.getSize();
03500             AccessTime date = searchInfo.getTime();
03501 
03502             QString stringSize;
03503             stringSize += QString::number( size );
03504 
03505             //in the following lines, the method QString::rightJustify is used
03506             //to ensure formatting of date with the right amount of digits for
03507             //each parameter
03508             QString stringDate;
03509             stringDate += QString::number( date.Year ).rightJustify( 4, '0' );
03510             stringDate += QString::number( date.Month ).rightJustify( 2, '0' );
03511             stringDate += QString::number( date.Day ).rightJustify( 2, '0' );
03512             stringDate += QString::number( date.Hour ).rightJustify( 2, '0' );
03513             stringDate += QString::number( date.Minute ).rightJustify( 2, '0' );
03514 
03515             //relative path (to the main cache directory) of the file
03516             QString searchName = QString( objectName );
03517             searchName.remove( 0, srcDirectory.length() );
03518             searchName.remove( 0, 1 );
03519             
03520             vector<QDomElement>::iterator it;
03521             for( it = md5Cache.begin(); it != md5Cache.end() && !found; it++ )
03522             {
03523                 if( (*it).attribute( "name" ) == searchName )
03524                 //found file on the cache
03525                 {
03526                     if( searchInfo.isDir() )
03527                     //don't check consistency for directories
03528                     {
03529                         md5sum = NULL;
03530                         return true;
03531                     }
03532 
03533                     found = true;
03534 
03535                     //CASE 1: consistency OK
03536                     if( stringSize == (*it).attribute( "size" ) &&
03537                         stringDate == (*it).attribute( "date" ) )
03538                     {
03539                         #ifdef RIO_DEBUG2
03540                         RioErr << "CASE 1: cache updated" << endl;
03541                         #endif
03542 
03543                         strcpy( md5sum,
03544                         (char *)(*it).attribute( "md5sum" ).latin1() );
03545                         //Why explicit typecast?
03546                     }
03547                     //CASE 2: consistency not OK: update cache
03548                     else
03549                     {
03550                         #ifdef RIO_DEBUG2
03551                         RioErr << "CASE 2: cache outdated" << endl;
03552                         RioErr << " file size = " << stringSize.latin1()
03553                                << ",  file date = " << stringDate.latin1() << endl;
03554                         RioErr << "cache size = " << (*it).attribute( "size" ).latin1()
03555                                << ", cache date = " << (*it).attribute( "date" ).latin1() << endl;
03556                         #endif
03557 
03558                         (*it).setAttribute( "size", stringSize );
03559                         (*it).setAttribute( "date", stringDate );
03560 
03561                         char buffer[MD5SIZE];
03562                         parent->setMD5Calculation( objectName, false );
03563                         QString md5 = calculateMD5( objectName, buffer );
03564                         parent->setMD5Calculation( objectName, true );
03565                         (*it).setAttribute( "md5sum", md5 );
03566                         strcpy( md5sum, (char *)md5.latin1() );
03567                     }
03568                     return true;
03569                 }
03570             }
03571             //CASE 3: file not found on the cache: add it
03572             if( !found )
03573             {
03574                 #ifdef RIO_DEBUG2
03575                 RioErr << "CASE 3: no entry on cache" << endl;
03576                 #endif
03577 
03578                 char buffer[MD5SIZE];
03579                 parent->setMD5Calculation( objectName, false );
03580                 QString md5 = calculateMD5( objectName, buffer );
03581                 parent->setMD5Calculation( objectName, true );
03582 
03583                 if( searchInfo.isDir() ) //md5sum of a directory is NULL
03584                 {
03585                     md5sum = NULL;
03586                     QDomDocument doc( "document" );
03587                     QDomElement newElement = doc.createElement( "dir" );
03588                     newElement.setAttribute( "name", searchName );
03589                     md5Cache.push_back( newElement );
03590 
03591                     return true;
03592                 }
03593 
03594                 QDomDocument doc( "document" );
03595                 QDomElement newElement = doc.createElement( "file" );
03596                 newElement.setAttribute( "name", searchName );
03597                 newElement.setAttribute( "md5sum", md5 );
03598                 newElement.setAttribute( "size", stringSize );
03599                 newElement.setAttribute( "date", stringDate );
03600                 md5Cache.push_back( newElement );
03601 
03602                 strcpy( md5sum, (char *)md5.latin1() );
03603                 return true;
03604                 }
03605         }
03606         // CASE 4: cache not used
03607         else
03608         {
03609             #ifdef RIO_DEBUG2
03610             RioErr << "CASE 4: cache not used" << endl;
03611             #endif
03612 
03613             if( searchInfo.isDir() ) //md5sum of a directory is NULL
03614             {
03615                 md5sum = NULL;
03616                 return true;
03617             }
03618             char buffer[MD5SIZE];
03619             parent->setMD5Calculation( objectName, false );
03620             QString md5 = calculateMD5( objectName, buffer );
03621             parent->setMD5Calculation( objectName, true );
03622             strcpy( md5sum, (char *)md5.latin1() );
03623             return true;
03624         }
03625     }
03626     // CASE 5: REMOTE or SERVER object
03627     else
03628     {
03629         #ifdef RIO_DEBUG2
03630         RioErr << "CASE 5: not a local file" << endl;
03631         #endif
03632 
03633         searchInfo.getMd5sum( md5sum );
03634         return true;
03635     }
03636     //any existing file must fit any of the five cases; otherwise, it's an error
03637     md5sum = NULL;
03638     return false;
03639 }

bool RioMd5::importData ( QString  srcDirectory,
CRioSession srcSession 
)

importData gets data from all files in srcDirectory and stores on md5Cache if RioMd5 was created with cache; used to load cache from remote md5 data

Definition at line 3740 of file RioExplorer.cpp.

03741 {
03742     #ifdef RIO_DEBUG2
03743     RioErr << "Importing MD5 data from the RIO Session" << endl;
03744     #endif
03745 
03746     if( !srcDirectory.isNull() ) //cache created
03747     {
03748         md5Cache.clear();
03749 
03750         ObjectInfo dirInfo;
03751         vector<ObjectInfo> objInfo;
03752         vector<ObjectInfo> localObjInfo;
03753         vector<ObjectInfo>::iterator objIterator;
03754         vector<ObjectInfo>::iterator localObjIterator;
03755 
03756         if( !parent->getObjectInfo( (char*)srcDirectory.latin1(), srcSession,
03757             &dirInfo, false ) )
03758             return false;
03759 
03760         if( !parent->ls( &dirInfo, &objInfo, srcSession, true, true ) )
03761             return false;
03762 
03763         if( !parent->getObjectInfo( this->srcDirectory.latin1(), md5Session,
03764             &dirInfo, false ) )
03765             return false;
03766 
03767         if( !parent->ls( &dirInfo, &localObjInfo, md5Session, true, true ) )
03768             return false;
03769 
03770         for( objIterator = objInfo.begin();
03771              objIterator != objInfo.end();
03772              objIterator++ )
03773         {
03774             char          md5[MD5SIZE];
03775             QString       relativePath;
03776             QString       stringSize;
03777             QString       stringDate;
03778             AccessTime    date;
03779             RioObjectSize size;
03780 
03781             if( objIterator->isDir() )
03782             {
03783                 relativePath = objIterator->getFullName();
03784 
03785                 QDomDocument doc( "document" );
03786                 QDomElement newElement = doc.createElement( "dir" );
03787                 newElement.setAttribute( "name", relativePath );
03788                 md5Cache.push_back( newElement );
03789             }
03790             else
03791             {
03792                 relativePath = objIterator->getFullName();
03793 
03794                 size = objIterator->getSize();
03795                 stringSize += QString::number( size );
03796 
03797                 objIterator->getMd5sum( md5 );
03798                 // Unlike the "name", "md5sum" and "size" properties, the "date"
03799                 // property does not need to be equal for both files in order
03800                 // for them to match. We must use the local file as source of
03801                 // this property's value, or we run the risk of causing an
03802                 // inconsistency when checking whether the MD5Sum needs to be
03803                 // recalculated.
03804                 for( localObjIterator = localObjInfo.begin();
03805                      localObjIterator != localObjInfo.end();
03806                      localObjIterator++ )
03807                     if( localObjIterator->getFullName() == objIterator->getFullName() )
03808                         break;
03809 
03810                 date = localObjIterator->getTime();
03811 
03812                 // In the following lines, the method QString::rightJustify is
03813                 // used to ensure formatting of date with the right amount of
03814                 // digits for each parameter.
03815             stringDate += QString::number( date.Year ).rightJustify( 4, '0' );
03816             stringDate += QString::number( date.Month ).rightJustify( 2, '0' );
03817             stringDate += QString::number( date.Day ).rightJustify( 2, '0' );
03818             stringDate += QString::number( date.Hour ).rightJustify( 2, '0' );
03819             stringDate += QString::number( date.Minute ).rightJustify( 2, '0' );
03820 
03821                 QDomDocument doc( "document" );
03822                 QDomElement newElement = doc.createElement( "file" );
03823                 newElement.setAttribute( "name", relativePath );
03824                 newElement.setAttribute( "md5sum", md5 );
03825                 newElement.setAttribute( "size", stringSize );
03826                 newElement.setAttribute( "date", stringDate );
03827                 md5Cache.push_back( newElement );
03828             }
03829         }
03830     }
03831     return true;
03832 }

void RioMd5::loadCache ( QString  src,
vector< QDomElement > &  md5Cache 
) [private]

loadCache carrega todos os itens armazenados na cache do diretório src e de todos os seus subdiretórios no vetor md5Cache.

Definition at line 3397 of file RioExplorer.cpp.

03398 {
03399     //loading main XML structure
03400     QString cacheFile = src + '/' + MD5CACHEFILE;
03401     QDomDocument doc( cacheFile );
03402     QFile file( cacheFile );
03403     #ifdef RIO_DEBUG2
03404     RioErr << "Loading cache: " << cacheFile.latin1() << endl;
03405     #endif
03406     if ( !file.open( IO_ReadWrite ) )
03407     {
03408         #ifdef RIO_DEBUG2
03409         RioErr << "Could not open/create MD5Sum cache file." << endl;
03410         #endif
03411         return;
03412     }
03413     if ( !doc.setContent( &file ) )
03414     {
03415         #ifdef RIO_DEBUG2
03416         RioErr << "Could not set content: probably a corrupted or empty MD5Sum "
03417                << "cache file."
03418                << endl;
03419         #endif
03420         file.close();
03421         return;
03422     }
03423 
03424     //for each element:
03425     //1) add it to the cache vector
03426     //2) if it's a directory, load its cache too
03427     QDomNode node = doc.documentElement().firstChild();
03428     while( !node.isNull() )
03429     {
03430         if( node.isElement() )
03431         {
03432             QDomElement elem = node.toElement();
03433             QString path = src.copy();
03434 
03435             //getting relative path (to the main cache directory) of the file
03436             path.remove( 0, srcDirectory.length() );
03437             path.remove( 0, 1 );
03438             if( !path.isEmpty() ) path += '/';
03439             path += elem.attribute( "name" );
03440             elem.setAttribute( "name", path );
03441 
03442             md5Cache.push_back( elem );
03443             if( ( elem.tagName() == "dir" ) )
03444                 loadCache( src + "/" +
03445                                elem.attribute( "name" ).section( '/', -1 ),
03446                            md5Cache );
03447         }
03448         node = node.nextSibling();
03449     }
03450     file.close();
03451 }

bool RioMd5::updateCache (  ) 

This method ensures that the cache, if existant, is fully updated.

That is, a file is present in the cache if, an only if, it exists in srcDirectory.

Definition at line 3645 of file RioExplorer.cpp.

03646 {
03647     #ifdef RIO_DEBUG1
03648     RioErr << "Entered updateCache" << endl;
03649     #endif
03650 
03651     if( !parent->checkSession( md5Session ) )
03652         return false;
03653 
03654     #ifdef RIO_DEBUG2
03655     if( !srcDirectory.isEmpty() )
03656         RioErr << "Updating cache for " << srcDirectory.latin1() << endl;
03657     #endif
03658 
03659     if( !srcDirectory.isEmpty() ) //cache is used
03660     {
03661         ObjectInfo objInfo;
03662         vector<ObjectInfo> objectsInfo;
03663         vector<ObjectInfo>::iterator lsIterator;
03664         vector<QDomElement>::iterator cacheIterator;
03665         char md5Buffer[MD5SIZE];    
03666         memset( md5Buffer, 0, sizeof( md5Buffer ) );
03667 
03668         if( !parent->getObjectInfo( (char*)srcDirectory.latin1(), md5Session,
03669                                     &objInfo ) ||
03670             !parent->ls( &objInfo, &objectsInfo, md5Session, true, true )
03671           )
03672         {
03673             #ifdef RIO_DEBUG1
03674             RioErr << "Leaving updateCache [1]" << endl;
03675             #endif
03676             return false;
03677         }
03678 
03679         //this loop gets the MD5 checksum for all files in srcDirectory. This
03680         //will ensure the cache contains all existent files
03681         for( lsIterator = objectsInfo.begin(); lsIterator != objectsInfo.end();
03682             lsIterator++ )
03683         {
03684             if( (char *)MD5CACHEFILE.latin1() != lsIterator->getName() )
03685             {
03686                 QString fileName = srcDirectory + '/' +
03687                                                       lsIterator->getFullName();
03688                 getMd5Sum( (char *)fileName.latin1(), md5Buffer );
03689             }
03690         }
03691         //this loop removes from the cache all files that no inter exist
03692         for( cacheIterator = md5Cache.begin();
03693              cacheIterator != md5Cache.end(); )
03694         {
03695             QString fileName = srcDirectory + '/' +
03696                                              cacheIterator->attribute( "name" );
03697             if( !parent->getObjectInfo( (char *)fileName.latin1(), md5Session,
03698                     &objInfo ) )
03699                  md5Cache.erase( cacheIterator );
03700             else
03701                 cacheIterator++;
03702         }
03703         #ifdef RIO_DEBUG1
03704         RioErr << "Leaving updateCache [2]" << endl;
03705         #endif
03706         return true;
03707     }
03708     else //cache isn't used: no need to update
03709     {
03710         #ifdef RIO_DEBUG1
03711         RioErr << "Leaving updateCache [3]" << endl;
03712         #endif
03713         return true;
03714     }
03715 }


Field Documentation

vector<QDomElement> RioMd5::md5Cache [private]

Definition at line 244 of file RioExplorer.h.

QString RioMd5::MD5CACHEFILE [private]

Definition at line 245 of file RioExplorer.h.

Definition at line 242 of file RioExplorer.h.

Definition at line 241 of file RioExplorer.h.

QString RioMd5::srcDirectory [private]

Definition at line 243 of file RioExplorer.h.


The documentation for this class was generated from the following files:
Generated on Wed Jul 4 16:03:36 2012 for RIO by  doxygen 1.6.3