From 0e5966272921cc8253e4395fcf985d490f27de0b Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 18 Feb 2004 22:11:41 +0000 Subject: [PATCH] Added zlint --- Makefile.am | 2 +- configure.in | 1 + zlint/Makefile.am | 9 + zlint/zlint.cpp | 816 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 827 insertions(+), 1 deletion(-) create mode 100644 zlint/Makefile.am create mode 100644 zlint/zlint.cpp diff --git a/Makefile.am b/Makefile.am index 0e1b58e..a7d9a15 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,6 +1,6 @@ AUTOMAKE_OPTIONS = foreign -SUBDIRS = src include zoom etc doc +SUBDIRS = src include zlint zoom etc doc aclocaldir=$(datadir)/aclocal aclocal_DATA = yazpp.m4 diff --git a/configure.in b/configure.in index e29ffe5..7ea8a4f 100644 --- a/configure.in +++ b/configure.in @@ -101,6 +101,7 @@ AC_OUTPUT([ src/Makefile include/Makefile include/yaz++/Makefile + zlint/Makefile yaz++-config zoom/Makefile doc/Makefile diff --git a/zlint/Makefile.am b/zlint/Makefile.am new file mode 100644 index 0000000..b0ebe93 --- /dev/null +++ b/zlint/Makefile.am @@ -0,0 +1,9 @@ +## $Id: Makefile.am,v 1.1 2004-02-18 22:11:41 adam Exp $ + +AM_CXXFLAGS = $(YAZINC) -I$(srcdir)/../include $(XSLT_CFLAGS) + +zlint_SOURCES=zlint.cpp + +bin_PROGRAMS = zlint + +LDADD=../src/libyazcpp.la $(YAZLALIB) $(XSLT_LIBS) diff --git a/zlint/zlint.cpp b/zlint/zlint.cpp new file mode 100644 index 0000000..e50de1e --- /dev/null +++ b/zlint/zlint.cpp @@ -0,0 +1,816 @@ +/* + * Copyright (c) 2004, Index Data. + * See the file LICENSE for details. + * + * $Id: zlint.cpp,v 1.1 2004-02-18 22:11:41 adam Exp $ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define REFID_BUF1 "zlint\000check1" +#define REFID_LEN1 12 +#define REFID_BUF2 "zlint\000check2" +#define REFID_LEN2 12 + +enum test_code { + TEST_FINISHED, + TEST_CONTINUE, +}; + +#if 0 +class Zlint; +class Zlint_test { +public: + virtual void init_send(Zlint *z) = 0; + virtual test_code init_recv(Zlint *z, Z_InitResponse *ir) = 0; + virtual test_code other_recv(Zlint *z, Z_APDU *ir, Z_InitResponse *ir) = 0; +}; +#endif + +const char *try_syntax [] = { + "usmarc", + "unimarc", + "danmarc", + "sutrs", + "grs1", + "xml", + "normarc", + 0 +}; +const char *try_query[] = { + "@attr 1=4 petersson", + "@attr 1=1016 petersson", + "@attr 1=4 kingdom", + "@attr 1=1016 kingdom", + "@attr 1=62 sword", + "sword" + "seven", + "@attr 1=4 water", + "@attr 1=1016 water", + "computer", + "@attr 1=4 computer", + "@attr 1=1016 computer", + "water", + "join", + "about", + "map", + 0, +}; + +const char *try_sort [] = { + "1=4 <", + "1=4 >", + "1=62 >", + "1=62 <", + 0 +}; +const char *try_scan [] = { + "@attr 1=4 ab", + "@attr 1=1003 ab", + "@attr 1=1016 ab", + 0 +}; + +class Zlint : public Yaz_Z_Assoc { + int m_tst_no; + + int m_subtst_no; + + int m_query_no; + int m_scan_no; + int m_sort_no; + int m_record_syntax_no; + int m_got_result_set; + IYaz_PDU_Observable *m_PDU_Observable; + char *m_host; + char *m_database; + int m_timeout_init; + int m_timeout_connect; + int m_protocol_version; + char m_session_str[20]; + int initResponseGetVersion(Z_InitResponse *init); +public: + void prepare(); + void recv_GDU(Z_GDU *apdu, int len); + Zlint(IYaz_PDU_Observable *the_PDU_Observable); + void args(int argc, char **argv); + void connectNotify(); + void failNotify(); + void closeNextTest(); + void sendTest(); + int nextTest(); + void testContinue(); + void timeoutNotify(); + IYaz_PDU_Observer *sessionNotify( + IYaz_PDU_Observable *the_PDU_Observable, int fd); + void connect(); + Z_ReferenceId *mk_refid(const char *buf, int len); +}; + +int Zlint::initResponseGetVersion(Z_InitResponse *init) +{ + int no = 0; + int off = 0; + int i; + for (i = 0; i<12; i++) + if (ODR_MASK_GET(init->protocolVersion, no)) + { + no = i+1; + if (off) + yaz_log(LOG_WARN, "%sbad formatted version"); + } + else + off = 1; + return no; +} + +Z_ReferenceId *Zlint::mk_refid(const char *buf, int len) +{ + Z_ReferenceId *id = + (Z_ReferenceId *) odr_malloc(odr_encode(), sizeof(*id)); + id->size = id->len = len; + id->buf = (unsigned char*) odr_malloc(odr_encode(), len); + memcpy(id->buf, buf, len); + return id; +} + +void Zlint::recv_GDU(Z_GDU *gdu, int len) +{ + yaz_log(LOG_LOG, "%sgot PDU", m_session_str); + if (gdu->which != Z_GDU_Z3950) + { + yaz_log(LOG_LOG, "%sreceived non-Z39.50 response", m_session_str); + closeNextTest(); + } + if (gdu->u.z3950 && gdu->u.z3950->which == Z_APDU_initResponse) + { + int i; + Z_InitResponse *init = gdu->u.z3950->u.initResponse; + int ver = initResponseGetVersion(init); + int result = init->result ? *init->result : 0; + if (!result) + yaz_log(LOG_WARN, "%sinit rejected"); + switch(m_tst_no) + { + case 0: + if (ver > 3 || ver < 2) + yaz_log(LOG_WARN, "%sgot version %d, expected 2 or 3", + m_session_str, ver); + m_protocol_version = ver; + if (!result) + closeNextTest(); + else + { + close(); + nextTest(); + connect(); + } + break; + case 1: + if (ver != 2) + yaz_log(LOG_WARN, "%sgot version %d, expected 2", + m_session_str, ver); + closeNextTest(); + break; + case 2: + if (ver < 2 || ver > 5) + yaz_log(LOG_WARN, "%sgot version %d, expected 2-5", + m_session_str,ver); + closeNextTest(); + break; + case 3: + if (!init->referenceId) + yaz_log(LOG_WARN, "%smissing referenceID from init response", + m_session_str); + else if (init->referenceId->len != REFID_LEN1 + || memcmp(init->referenceId->buf, REFID_BUF1, REFID_LEN1)) + yaz_log(LOG_WARN, "%sreference ID does not match"); + closeNextTest(); + break; + case 4: + if (m_subtst_no == 0) + { + if (!init->referenceId) + yaz_log(LOG_WARN, "%smissing referenceID from first init response", + m_session_str); + else if (init->referenceId->len != REFID_LEN1 + || memcmp(init->referenceId->buf, REFID_BUF1, REFID_LEN1)) + yaz_log(LOG_WARN, "%sreference ID does not match"); + m_subtst_no++; + } + else + { + if (!init->referenceId) + yaz_log(LOG_WARN, "%smissing referenceID from second init response", + m_session_str); + else if (init->referenceId->len != REFID_LEN2 + || memcmp(init->referenceId->buf, REFID_BUF2, REFID_LEN2)) + yaz_log(LOG_WARN, "%sreference ID does not match"); + closeNextTest(); + } + break; + case 5: + if (init->options) + { + int i; + int no_set = 0; + int no_reset = 0; + for (i = 0; i <= 24; i++) + if (ODR_MASK_GET(init->options, i)) + no_set++; + else + no_reset++; + if (no_set < 2) + yaz_log(LOG_WARN, "%ssuspicuously few option bits set", + m_session_str); + if (no_reset == 0) + yaz_log(LOG_WARN, "%ssuspicuously many option bits set", + m_session_str); + } + closeNextTest(); + break; + case 6: + if (ODR_MASK_GET(init->options, Z_Options_negotiationModel)) + { + Z_CharSetandLanguageNegotiation *p = + yaz_get_charneg_record(init->otherInfo); + + if (p) { + + char *charset=NULL, *lang=NULL; + int selected; + NMEM m = nmem_create(); + + yaz_get_response_charneg(m, p, &charset, &lang, + &selected); + yaz_log(LOG_LOG, "%sAccepted character set : %s", + m_session_str, charset); + yaz_log(LOG_LOG, "%sAccepted code language : %s", + m_session_str, lang ? lang : "none"); + yaz_log(LOG_LOG, "%sAccepted records in ...: %d", + m_session_str, selected ); + nmem_destroy(m); + } + } + closeNextTest(); + break; + case 7: + if (m_subtst_no * m_subtst_no * 100000 + 2000 < *init->maximumRecordSize) + yaz_log(LOG_WARN, "%smaximumRecordSize bigger than proposed size"); + + if (m_subtst_no * m_subtst_no * 100000 + 2000 < *init->preferredMessageSize) + yaz_log(LOG_WARN, "%smaximumRecordSize bigger than proposed size"); + if (m_subtst_no < 3) + { + close(); + m_subtst_no++; + connect(); + } + else + closeNextTest(); + break; + case 9: + if (result && ODR_MASK_GET(init->options, Z_Options_scan)) + sendTest(); + else + closeNextTest(); + break; + case 10: + if (result && ODR_MASK_GET(init->options, Z_Options_sort)) + sendTest(); + else + closeNextTest(); + break; + default: + if (result) + sendTest(); + else + closeNextTest(); + } + } + else if (gdu->u.z3950 && gdu->u.z3950->which == Z_APDU_searchResponse) + { + Z_SearchResponse *sr = gdu->u.z3950->u.searchResponse; + switch(m_tst_no) + { + case 8: + if (sr->records && (sr->records->which == Z_Records_NSD + || + sr->records->which == Z_Records_multipleNSD)) + { + yaz_log(LOG_WARN, "%sSearch Error", m_session_str); + m_query_no++; + sendTest(); + } + else if (!sr->resultCount || *sr->resultCount == 0) + { + m_query_no++; + sendTest(); + } + else + { + yaz_log(LOG_LOG, "%sgot %d result count with %s", + m_session_str, *sr->resultCount, + try_query[m_query_no]); + m_got_result_set = 1; + sendTest(); + } + break; + default: + closeNextTest(); + } + } + else if (gdu->u.z3950 && gdu->u.z3950->which == Z_APDU_presentResponse) + { + Z_PresentResponse *sr = gdu->u.z3950->u.presentResponse; + switch(m_tst_no) + { + case 8: + if (sr->records && (sr->records->which == Z_Records_NSD + || + sr->records->which == Z_Records_multipleNSD)) + { + yaz_log(LOG_LOG, "%spresent returned NSD for %s", + m_session_str, try_syntax[m_record_syntax_no]); + } + else if (sr->records && sr->records->which == Z_Records_DBOSD + && sr->records->u.databaseOrSurDiagnostics->num_records>0 + && sr->records->u.databaseOrSurDiagnostics->records[0]) + { + if (sr->records->u.databaseOrSurDiagnostics->records[0]->which == Z_NamePlusRecord_databaseRecord) + { + Z_External *ext = sr->records->u.databaseOrSurDiagnostics->records[0]->u.databaseRecord; + Odr_oid *expectRecordSyntax = + yaz_str_to_z3950oid(odr_decode(), CLASS_RECSYN, + try_syntax[m_record_syntax_no]); + if (oid_oidcmp(expectRecordSyntax, + ext->direct_reference)) + yaz_log(LOG_WARN, "%spresent bad record type for %s", + m_session_str, + try_syntax[m_record_syntax_no]); + else + yaz_log(LOG_LOG, "%spresent OK for %s", m_session_str, + try_syntax[m_record_syntax_no]); + } + else if (sr->records->u.databaseOrSurDiagnostics->records[0]->which == Z_NamePlusRecord_surrogateDiagnostic) + yaz_log(LOG_LOG, "%spresent returned SD %s", m_session_str, + try_syntax[m_record_syntax_no]); + else + yaz_log(LOG_WARN, "%spresent returned fragment %s", + m_session_str, + try_syntax[m_record_syntax_no]); + } + else + { + yaz_log(LOG_WARN, "%spresent returned no records or diagnostics", m_session_str); + + } + m_record_syntax_no++; + sendTest(); + } + } + else if (gdu->u.z3950 && gdu->u.z3950->which == Z_APDU_scanResponse) + { + Z_ScanResponse *sr = gdu->u.z3950->u.scanResponse; + switch(m_tst_no) + { + case 9: + if (sr->entries->nonsurrogateDiagnostics) + { + yaz_log(LOG_LOG, "%sscan NSD for %s", m_session_str, + try_scan[m_scan_no]); + m_scan_no++; + sendTest(); + } + else if (sr->entries->entries && sr->entries->num_entries > 0) + { + yaz_log(LOG_LOG, "%sscan OK for %s", m_session_str, + try_scan[m_scan_no]); + closeNextTest(); + } + else + { + yaz_log(LOG_WARN, "%sscan no entries/diagnostics for %s", + m_session_str, + try_scan[m_scan_no]); + m_scan_no++; + sendTest(); + } + break; + default: + closeNextTest(); + } + } + else if (gdu->u.z3950 && gdu->u.z3950->which == Z_APDU_sortResponse) + { + Z_SortResponse *sr = gdu->u.z3950->u.sortResponse; + switch(m_tst_no) + { + case 10: + if (sr->diagnostics) + { + yaz_log(LOG_LOG, "%ssort NSD for %s", m_session_str, + try_sort[m_sort_no]); + m_sort_no++; + sendTest(); + } + else + { + yaz_log(LOG_LOG, "%ssort OK for %s", m_session_str, + try_sort[m_sort_no]); + closeNextTest(); + } + break; + default: + closeNextTest(); + } + } + else + closeNextTest(); +} + +Zlint::Zlint(IYaz_PDU_Observable *the_PDU_Observable) : + Yaz_Z_Assoc(the_PDU_Observable) +{ + m_PDU_Observable = the_PDU_Observable; + m_host = 0; + m_database = 0; + m_timeout_connect = 30; + m_timeout_init = 30; + m_tst_no = 0; + m_subtst_no = 0; + m_protocol_version = 0; + sprintf(m_session_str, "%d ", m_tst_no); +} + +void Zlint::connectNotify() +{ + Z_APDU *apdu = create_Z_PDU(Z_APDU_initRequest); + Z_InitRequest *init = apdu->u.initRequest; + int len; + Z_OtherInformation **oi; + + timeout(m_timeout_init); + + switch(m_tst_no) + { + case 0: + /* check if target properly negotiates to v3 .. */ + ODR_MASK_ZERO(init->protocolVersion); + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_1); + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_2); + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + break; + case 1: + /* check if target properly negotiates to v2 .. */ + ODR_MASK_ZERO(init->protocolVersion); + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_1); + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_2); + break; + case 2: + /* check latest version of target - up to v9 */ + ODR_MASK_ZERO(init->protocolVersion); + int i; + for (i = 0; i< 9; i++) + ODR_MASK_SET(init->protocolVersion, i); + break; + case 3: + /* send refID in init request */ + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + init->referenceId = mk_refid(REFID_BUF1, REFID_LEN1); + break; + case 4: + /* send double init with differnet refID's */ + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + ODR_MASK_SET(init->options, Z_Options_concurrentOperations); + init->referenceId = mk_refid(REFID_BUF1, REFID_LEN1); + send_Z_PDU(apdu, &len); + + apdu = create_Z_PDU(Z_APDU_initRequest); + init = apdu->u.initRequest; + + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + ODR_MASK_SET(init->options, Z_Options_concurrentOperations); + + init->referenceId = mk_refid(REFID_BUF2, REFID_LEN2); + break; + case 5: + /* set all options.. see what target really supports .. */ + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + ODR_MASK_ZERO(init->options); + for (i = 0; i <= 24; i++) + ODR_MASK_SET(init->options, i); + break; + case 6: + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + yaz_oi_APDU(apdu, &oi); + if (oi) + { + Z_OtherInformationUnit *p0; + const char *negotiationCharset[] = { + "UTF-8", + "UTF-16", + "UCS-2", + "UCS-4", + "ISO-8859-1" + }; + char *yazLang = 0; + + if ((p0=yaz_oi_update(oi, odr_encode(), NULL, 0, 0))) { + ODR_MASK_SET(init->options, Z_Options_negotiationModel); + + p0->which = Z_OtherInfo_externallyDefinedInfo; + p0->information.externallyDefinedInfo = + + yaz_set_proposal_charneg( + odr_encode(), + negotiationCharset, 5, + (const char**)&yazLang, yazLang ? 1 : 0, 1); + } + } + break; + case 7: + *init->maximumRecordSize = m_subtst_no * m_subtst_no* 100000 + 2000; + *init->preferredMessageSize = m_subtst_no * m_subtst_no *100000 + 2000; + break; + case 8: + /* search */ + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + ODR_MASK_SET(init->options, Z_Options_namedResultSets); + break; + case 9: + /* scan */ + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + ODR_MASK_SET(init->options, Z_Options_namedResultSets); + ODR_MASK_SET(init->options, Z_Options_scan); + break; + case 10: + /* sort */ + ODR_MASK_SET(init->protocolVersion, Z_ProtocolVersion_3); + ODR_MASK_SET(init->options, Z_Options_namedResultSets); + ODR_MASK_SET(init->options, Z_Options_sort); + break; + } + int r = send_Z_PDU(apdu, &len); +} + +int Zlint::nextTest() +{ + m_subtst_no = 0; + while(m_tst_no >= 0) + { + m_tst_no++; + sprintf(m_session_str, "%d ", m_tst_no); + switch(m_tst_no) + { + case 0: + return 1; + case 1: + return 1; + case 2: + return 1; + case 3: + return 1; + case 4: + return 1; + case 5: + return 1; + case 6: + return 1; + case 7: + return 1; + case 8: + m_query_no = 0; + m_record_syntax_no = 0; + m_got_result_set = 0; + return 1; + case 9: + m_scan_no = 0; + return 1; + case 10: + m_sort_no = 0; + return 1; + default: + m_tst_no = -1; + } + } + return 0; +} + +// current test failed badly - goto next or stop.. +void Zlint::closeNextTest() +{ + close(); + if (m_tst_no != 0) + { + nextTest(); + connect(); + } +} + +void Zlint::failNotify() +{ + yaz_log(LOG_WARN, "%sconnection closed by foreign host", m_session_str); + testContinue(); +} + +void Zlint::timeoutNotify() +{ + yaz_log(LOG_WARN, "%sconnection timed out", m_session_str); + testContinue(); +} + +void Zlint::testContinue() +{ + close(); + switch(m_tst_no) + { + case 8: + if (m_got_result_set) + { + // must search again to establish.. keep query + m_got_result_set = 0; + m_record_syntax_no++; + } + else + { + // try new search .. + m_query_no++; + } + connect(); + return; + case 9: + m_scan_no++; + connect(); + return; + } + nextTest(); +} + +void Zlint::sendTest() +{ + Z_APDU *apdu; + switch(m_tst_no) + { + case 8: + if (m_got_result_set == 0) + { + apdu = zget_APDU(odr_encode(), Z_APDU_searchRequest); + Z_SearchRequest *sr; + sr = apdu->u.searchRequest; + sr->query = (Z_Query *) odr_malloc(odr_encode(), sizeof(*sr->query)); + if (try_query[m_query_no] && sr) + { + sr->query->which = Z_Query_type_1; + Z_RPNQuery *rpn; + YAZ_PQF_Parser pqf_parser = yaz_pqf_create (); + + sr->databaseNames = &m_database; + sr->num_databaseNames = 1; + + rpn = yaz_pqf_parse(pqf_parser, odr_encode(), try_query[m_query_no]); + + yaz_pqf_destroy (pqf_parser); + + if (rpn) + { + int len; + yaz_log(LOG_LOG, "%spqf: %s", + m_session_str, try_query[m_query_no]); + + sr->query->u.type_1 = rpn; + send_Z_PDU(apdu, &len); + } + else + closeNextTest(); + } + else + { + yaz_log(LOG_WARN, "%sunable to get any hit count", + m_session_str); + closeNextTest(); + } + } + else if (m_got_result_set && try_syntax[m_record_syntax_no]) + { + int len; + apdu = zget_APDU(odr_encode(), Z_APDU_presentRequest); + Z_PresentRequest *pr = apdu->u.presentRequest; + *pr->numberOfRecordsRequested = 1; + *pr->resultSetStartPoint = 1; + + pr->preferredRecordSyntax = + yaz_str_to_z3950oid(odr_encode(), CLASS_RECSYN, + try_syntax[m_record_syntax_no]); + send_Z_PDU(apdu, &len); + } + else + closeNextTest(); + break; + case 9: + apdu = zget_APDU(odr_encode(), Z_APDU_scanRequest); + if (apdu && try_scan[m_scan_no]) + { + int len; + YAZ_PQF_Parser pqf_parser = yaz_pqf_create (); + Z_ScanRequest *sr = apdu->u.scanRequest; + sr->termListAndStartPoint = yaz_pqf_scan(pqf_parser, + odr_encode(), + &sr->attributeSet, + try_scan[m_scan_no]); + + sr->databaseNames = &m_database; + sr->num_databaseNames = 1; + + yaz_pqf_destroy (pqf_parser); + send_Z_PDU(apdu, &len); + } + else + closeNextTest(); + break; + case 10: + apdu = zget_APDU(odr_encode(), Z_APDU_sortRequest); + if (apdu && try_sort[m_sort_no]) + { + char *setstring = "default"; + int len; + Z_SortRequest *sr = apdu->u.sortRequest; + + sr->num_inputResultSetNames = 1; + sr->num_inputResultSetNames = 1; + sr->inputResultSetNames = (Z_InternationalString **) + odr_malloc (odr_encode(), sizeof(*sr->inputResultSetNames)); + sr->inputResultSetNames[0] = odr_strdup (odr_encode(), setstring); + sr->sortedResultSetName = odr_strdup(odr_encode(), setstring); + sr->sortSequence = yaz_sort_spec(odr_encode(), try_sort[m_sort_no]); + send_Z_PDU(apdu, &len); + } + else + closeNextTest(); + break; + default: + closeNextTest(); + } +} + +IYaz_PDU_Observer *Zlint::sessionNotify( + IYaz_PDU_Observable *the_PDU_Observable, int fd) +{ + return 0; +} + +void Zlint::connect() +{ + if (m_host && m_tst_no != -1) + { + yaz_log(LOG_LOG, "%sconnecting to %s", m_session_str, m_host); + timeout(m_timeout_connect); + client(m_host); + } +} + +void Zlint::args(int argc, char **argv) +{ + char *arg; + int ret; + while ((ret = options("v", argv, argc, &arg)) != -2) + { + switch (ret) + { + case 'v': + break; + case 0: + if (arg) + { + const char *basep; + m_host = xstrdup(arg); + cs_get_host_args(m_host, &basep); + if (!basep || !*basep) + basep = "Default"; + m_database = xstrdup(basep); + } + break; + } + } +} + +int main(int argc, char **argv) +{ + Yaz_SocketManager mySocketManager; + Zlint z(new Yaz_PDU_Assoc(&mySocketManager)); + + z.args(argc, argv); + + z.connect(); + while (mySocketManager.processEvent() > 0) + ; + exit (0); +} -- 1.7.10.4