Version upgrade 4.00 #45

Merged
elB4RTO merged 113 commits from devel into main 2024-02-17 16:13:26 +01:00
2 changed files with 312 additions and 430 deletions
Showing only changes of commit ae92479fca - Show all commits

View file

@ -5,10 +5,10 @@
#include "modules/dialogs.h" #include "modules/dialogs.h"
#include "modules/database/database.h"
#include "utilities/io.h" #include "utilities/io.h"
#include <QString>
#include <QSqlQuery>
#include <QSqlError> #include <QSqlError>
#include <QVariant> #include <QVariant>
@ -16,6 +16,9 @@
#include <unordered_map> #include <unordered_map>
struct MakeNewDatabase {};
namespace CheckSec namespace CheckSec
{ {
@ -24,183 +27,158 @@ namespace /*private*/
//! Checks the tables' names integrity //! Checks the tables' names integrity
/*! /*!
\param db Database object, already initialized \param query Query instance from the target database
\param db_name Database's name, used by the dialogs if necessary \return Whether the database is valid or not
\return The result of the check: 0 if failed with an error, 1 if all the integrity checks passed, 2 if a rebuild is needed \throw LogDoctorException, MakeNewDatabase
\see checkCollectionDatabase(), checkHashesDatabase(), newCollectionDatabase(), newHashesDatabase() \see checkCollectionDatabase(), checkHashesDatabase(), newCollectionDatabase(), newHashesDatabase()
*/ */
int checkDatabaseTablesNames( QSqlDatabase& db, const QString& db_name ) noexcept bool checkDatabaseTablesNames( QueryWrapper query )
{ {
bool make_new{false}, ok{true}; query( QStringLiteral("SELECT name FROM sqlite_schema WHERE type = 'table';") );
QSqlQuery query{ QSqlQuery( db ) };
if ( ! query.exec("SELECT name FROM sqlite_schema WHERE type = 'table';") ) {
// error querying database
ok &= false;
DialogSec::errDatabaseFailedExecuting( db_name, query.lastQuery(), query.lastError().text() );
} else {
std::unordered_map<QString, bool> tables_checks{
{"apache", false},
{"nginx", false},
{"iis", false} };
while ( query.next() ) {
QString table_name{ query.value(0).toString() };
if ( tables_checks.find( table_name ) == tables_checks.end() ) {
// unexpected table name
if ( DialogSec::choiceDatabaseWrongTable( db_name, table_name ) ) {
// agreed to renew
make_new |= true;
} else {
// refused to renew
ok &= false;
}
break;
} else { std::unordered_map<QString, bool> checks{
// table found {"apache", false},
tables_checks.at( table_name ) |= true; {"nginx", false},
} {"iis", false} };
}
if ( ok && !make_new ) { while ( query->next() ) {
for ( const auto& [ tbl, res ] : tables_checks ) {
if ( ! res ) { const QString table{ query[0].toString() };
// a table has not been found
if ( DialogSec::choiceDatabaseMissingTable( db_name, tbl ) ) { if ( const auto tbl{ checks.find( table ) }; tbl != checks.end() ) {
// agreed to renew tbl->second |= true;
make_new |= true;
} else {
// refused to renew
ok &= false;
}
break;
}
}
}
tables_checks.clear();
}
query.finish();
if ( ok ) {
if ( make_new ) {
return 2;
} else { } else {
return 1; // unexpected table name
if ( DialogSec::choiceDatabaseWrongTable( query.dbName(), table ) ) {
// agreed to renew
throw MakeNewDatabase{};
} else {
// refused to renew
return false;
}
} }
} }
return 0;
for ( const auto& [tbl,res] : checks ) {
if ( ! res ) {
// a table has not been found
if ( DialogSec::choiceDatabaseMissingTable( query.dbName(), tbl ) ) {
// agreed to renew
throw MakeNewDatabase{};
} else {
// refused to renew
return false;
}
}
}
return true;
} }
//! Builds a new database for the logs Collection //! Builds a new database for the logs Collection
/*! /*!
\param db Database object, already initialized \param db Database object
\param db_name Database's name, used by the dialogs if necessary \param db_path The database file's path
\return The result of the operation \param ws_names Database's tables names
\see checkCollectionDatabase(), checkHashesDatabase() \return The result of the operation
\see checkCollectionDatabase(), checkHashesDatabase()
*/ */
bool newCollectionDatabase( QSqlDatabase& db, const QString& db_name, const std::vector<QString>& ws_names ) noexcept bool newCollectionDatabase( DatabaseWrapper db, const std::string& db_path, const std::vector<QString>& ws_names ) noexcept
{ {
bool successful{ true }; try {
// create the database
if ( ! db.open() ) { db.open( db_path, true );
// error opening database
successful &= false;
DialogSec::errDatabaseFailedOpening( db_name, db.lastError().text() );
} else {
// succesfully creted database file, now create the tables // succesfully creted database file, now create the tables
QSqlQuery query; const QString stmt{ QStringLiteral(R"(
CREATE TABLE "%1" (
"year" SMALLINT,
"month" TINYINT,
"day" TINYINT,
"hour" TINYINT,
"minute" TINYINT,
"second" TINYINT,
"protocol" TEXT,
"method" TEXT,
"uri" TEXT,
"query" TEXT,
"response" SMALLINT,
"time_taken" INTEGER,
"bytes_sent" INTEGER,
"bytes_received" INTEGER,
"client" TEXT,
"user_agent" TEXT,
"cookie" TEXT,
"referrer" TEXT
);)")};
for ( const QString& ws_name : ws_names ) { for ( const QString& ws_name : ws_names ) {
if ( ! successful ) { break; }
// compose the statement with the table name for the access logs QueryWrapper query{ db.getQuery() };
query.prepare( "\
CREATE TABLE \""+ws_name+"\" (\ if ( ! query->exec( stmt.arg( ws_name ) ) ) {
\"year\" SMALLINT,\
\"month\" TINYINT,\
\"day\" TINYINT,\
\"hour\" TINYINT,\
\"minute\" TINYINT,\
\"second\" TINYINT,\
\"protocol\" TEXT,\
\"method\" TEXT,\
\"uri\" TEXT,\
\"query\" TEXT,\
\"response\" SMALLINT,\
\"time_taken\" INTEGER,\
\"bytes_sent\" INTEGER,\
\"bytes_received\" INTEGER,\
\"client\" TEXT,\
\"user_agent\" TEXT,\
\"cookie\" TEXT,\
\"referrer\" TEXT\
);");
if ( ! query.exec() ) {
// error creating table // error creating table
successful &= false;
DialogSec::errDatabaseFailedExecuting( DialogSec::errDatabaseFailedExecuting(
QString( db_name ), db.name(),
QString("CREATE TABLE \"%1\" (...)").arg( ws_name ), QStringLiteral(R"(CREATE TABLE "%1" (...))").arg( ws_name ),
QString( query.lastError().text() ) ); query->lastError().text() );
throw LogDoctorException{};
} }
query.finish();
} }
// inform about creation } catch (...) {
if ( successful ) { DialogSec::errDatabaseFailedCreating( db.name() );
DialogSec::msgDatabaseCreated( db_name ); return false;
} else {
DialogSec::errDatabaseFailedCreating( db_name );
}
} }
return successful;
DialogSec::msgDatabaseCreated( db.name() );
return true;
} }
//! Builds a new database for the used log files' Hashes //! Builds a new database for the used log files' Hashes
/*! /*!
\param db Database object, already initialized \param db Database object, already initialized
\param db_name Database's name, used by the dialogs if necessary \param db_path The database file's path
\return The result of the operation \param ws_names Database's tables names
\see checkCollectionDatabase(), checkHashesDatabase() \return The result of the operation
\see checkCollectionDatabase(), checkHashesDatabase()
*/ */
bool newHashesDatabase( QSqlDatabase& db, const QString& db_name, const std::vector<QString>& ws_names ) noexcept bool newHashesDatabase( DatabaseWrapper db, const std::string& db_path, const std::vector<QString>& ws_names ) noexcept
{ {
bool successful{ true }; try {
// create the database
if ( ! db.open() ) { db.open( db_path, true );
// error opening database
successful &= false;
DialogSec::errDatabaseFailedOpening( db_name, db.lastError().text() );
} else {
// succesfully creted database file, now create the tables // succesfully creted database file, now create the tables
QSqlQuery query; const QString stmt{ QStringLiteral(R"(
CREATE TABLE "%1" (
"hash" TEXT
);)")};
for ( const QString& ws_name : ws_names ) { for ( const QString& ws_name : ws_names ) {
if ( ! successful ) { break; }
// compose the statement with the table name for the access logs QueryWrapper query{ db.getQuery() };
query.prepare( "\
CREATE TABLE \""+ws_name+"\" (\ if ( ! query->exec( stmt.arg( ws_name ) ) ) {
\"hash\" TEXT\
);");
if ( ! query.exec() ) {
// error creating table // error creating table
successful &= false;
DialogSec::errDatabaseFailedExecuting( DialogSec::errDatabaseFailedExecuting(
QString( db_name ), db.name(),
QString("CREATE TABLE \"%1\" (...)").arg( ws_name ), QStringLiteral(R"(CREATE TABLE "%1" (...))").arg( ws_name ),
QString( query.lastError().text() ) ); query->lastError().text() );
throw LogDoctorException{};
} }
query.finish();
} }
// inform about creation } catch (...) {
if ( successful ) { DialogSec::errDatabaseFailedCreating( db.name() );
DialogSec::msgDatabaseCreated( db_name ); return false;
} else {
DialogSec::errDatabaseFailedCreating( db_name );
}
} }
return successful;
DialogSec::msgDatabaseCreated( db.name() );
return true;
} }
} // namespace (private) } // namespace (private)
@ -208,314 +186,240 @@ bool newHashesDatabase( QSqlDatabase& db, const QString& db_name, const std::vec
bool checkCollectionDatabase( const std::string& db_path ) noexcept bool checkCollectionDatabase( const std::string& db_path ) noexcept
{ {
bool make_new{false}, ok{true};
std::error_code err;
QString err_msg;
const QString db_name{ QString::fromStdString( db_path.substr( db_path.find_last_of( '/' ) + 1 ) ) };
const std::vector<QString> ws_names{ "apache", "nginx", "iis" }; const std::vector<QString> ws_names{ "apache", "nginx", "iis" };
QSqlDatabase db{ QSqlDatabase::database(DatabasesNames::data) }; try {
db.setDatabaseName( QString::fromStdString( db_path ) );
// check the existence DatabaseWrapper db{ DatabaseHandler::get( DatabaseType::Data ) };
if ( IOutils::exists( db_path ) ) {
// check file type and permissions
if ( ! checkDatabaseFile( db_path, db_name ) ) {
ok &= false;
} else {
// database file seems ok, now try to open
if ( ! db.open() ) {
// error opening database
ok &= false;
DialogSec::errDatabaseFailedOpening( db_name, db.lastError().text() );
if ( ! IOutils::exists( db_path ) ) {
// ask to create a new one
if ( DialogSec::choiceDatabaseNotFound( db.name() ) ) {
// choosed to create it
throw MakeNewDatabase{};
} else { } else {
// database successfully opened, now check the tables // refused to create it, abort
const int check{ checkDatabaseTablesNames( db, db_name ) }; return false;
if ( check == 0 ) { }
ok &= false; }
} else if ( check == 2 ) {
make_new |= true; // check file type and permissions
if ( ! checkDatabaseFile( db_path, db.name() ) ) {
return false;
}
// file seems ok, try to open
db.open( db_path, true );
// check the tables
if ( ! checkDatabaseTablesNames( db.getQuery() ) ) {
return false;
}
const QString stmt{ QStringLiteral("SELECT name, type FROM pragma_table_info('%1') AS tbinfo;") };
// check every WebServer table
for ( const QString& table : ws_names ) {
bool has_warning_column{ false };
// column's name:type associations
std::unordered_map<QString, std::tuple<QString, bool>> checks {
{"year", { "SMALLINT", false} },
{"month", { "TINYINT", false} },
{"day", { "TINYINT", false} },
{"hour", { "TINYINT", false} },
{"minute", { "TINYINT", false} },
{"second", { "TINYINT", false} },
{"protocol", { "TEXT", false} },
{"method", { "TEXT", false} },
{"uri", { "TEXT", false} },
{"query", { "TEXT", false} },
{"response", { "SMALLINT", false} },
{"time_taken", { "INTEGER", false} },
{"bytes_sent", { "INTEGER", false} },
{"bytes_received", { "INTEGER", false} },
{"client", { "TEXT", false} },
{"user_agent", { "TEXT", false} },
{"cookie", { "TEXT", false} },
{"referrer", { "TEXT", false} }
};
QueryWrapper query{ db.getQuery() };
query( stmt.arg( table ) );
while ( query->next() ) {
const QString col_name{ query[0].toString() };
const QString col_type{ query[1].toString() };
if ( col_name == "warning" ) {
// provide backward compatibility, this column will be removed from the table
has_warning_column |= true;
} else if ( const auto it{ checks.find( col_name ) }; it != checks.end() ) {
// column found, check the data-type
auto& type{ it->second };
if ( col_type == std::get<0>( type ) ) {
// same data-type
std::get<1>( type ) |= true;
} else {
// different data-type, ask to renew
if ( DialogSec::choiceDatabaseWrongDataType( db.name(), table, col_name, col_type ) ) {
// agreed to renew
throw MakeNewDatabase{};
} else {
// refused to renew
return false;
}
}
} else {
// unexpected column
if ( DialogSec::choiceDatabaseWrongColumn( db.name(), table, col_name ) ) {
// agreed to renew
throw MakeNewDatabase{};
} else {
// refused to renew
return false;
}
} }
QSqlQuery query{ QSqlQuery( db ) }; }
if ( ok && !make_new ) {
// check every WebServer table, both access and error if ( has_warning_column ) {
for ( const QString& table : ws_names ) { // provide backward compatibility
query->finish();
query( QStringLiteral(R"(ALTER TABLE "%1" DROP COLUMN "warning";)").arg( table ) );
}
if ( !ok || make_new ) { break; } for ( const auto& [col,type] : checks ) {
if ( ! std::get<1>( type ) ) {
bool has_warning_column{ false }; // a column has not been found
// column's name:type associations if ( DialogSec::choiceDatabaseMissingColumn( db.name(), table, col ) ) {
std::unordered_map<QString, std::tuple<QString, bool>> // agreed to renew
data_types { throw MakeNewDatabase{};
{"year", { "SMALLINT", false} }, } else {
{"month", { "TINYINT", false} }, // refused to renew
{"day", { "TINYINT", false} }, return false;
{"hour", { "TINYINT", false} },
{"minute", { "TINYINT", false} },
{"second", { "TINYINT", false} },
{"protocol", { "TEXT", false} },
{"method", { "TEXT", false} },
{"uri", { "TEXT", false} },
{"query", { "TEXT", false} },
{"response", { "SMALLINT", false} },
{"time_taken", { "INTEGER", false} },
{"bytes_sent", { "INTEGER", false} },
{"bytes_received", { "INTEGER", false} },
{"client", { "TEXT", false} },
{"user_agent", { "TEXT", false} },
{"cookie", { "TEXT", false} },
{"referrer", { "TEXT", false} }
};
// query table's columns' infoes for access logs
if ( ! query.exec( "SELECT name, type FROM pragma_table_info('"+table+"') AS tbinfo;" ) ) {
// error opening database
ok &= false;
DialogSec::errDatabaseFailedExecuting( db_name, query.lastQuery(), query.lastError().text() );
}
// iterate over results
while ( query.next() ) {
const QString col_name{ query.value(0).toString() };
const QString col_type{ query.value(1).toString() };
if ( col_name == "warning" ) {
// provide backward compatibility, this column will be removed from the table
has_warning_column |= true;
} else if ( data_types.find( col_name ) == data_types.end() ) {
// unexpected column
if ( DialogSec::choiceDatabaseWrongColumn( db_name, table, col_name ) ) {
// agreed to renew
make_new |= true;
} else {
// refused to renew
ok &= false;
}
break;
} else {
// column found, check the data-type
const QString data_type{ std::get<0>( data_types.at( col_name ) ) };
if ( col_type == data_type ) {
// same data-type
data_types.at( col_name ) = std::tuple( data_type, true );
} else {
// different data-type, ask to renew
if ( DialogSec::choiceDatabaseWrongDataType( db_name, table, col_name, col_type ) ) {
// agreed to renew
make_new |= true;
} else {
// refused to renew
ok &= false;
}
break;
}
}
}
if ( ok && !make_new ) {
if ( has_warning_column ) {
// provide backward compatibility
query.finish();
if ( ! query.exec( "ALTER TABLE \""+table+"\" DROP COLUMN \"warning\";" ) ) {
// failed to remove the column
ok &= false;
DialogSec::errDatabaseFailedExecuting( db_name, query.lastQuery(), query.lastError().text() );
break;
}
}
for ( const auto& [ col, tup ] : data_types ) {
if ( ! std::get<1>( tup ) ) {
// a column has not been found
if ( DialogSec::choiceDatabaseMissingColumn( db_name, table, col ) ) {
// agreed to renew
make_new |= true;
} else {
// refused to renew
ok &= false;
}
break;
}
}
}
query.finish();
if ( !ok || make_new ) { break; }
} }
} }
} }
} }
} else { } catch (const MakeNewDatabase&) {
// database does not exist, yet, ask to create a new one
if ( DialogSec::choiceDatabaseNotFound( QString(db_name) ) ) {
// choosed to create it
make_new |= true;
} else {
// refused to create it, abort
ok &= false;
}
}
if ( ok && make_new ) {
// rename the current db file as a 'copy'
if ( IOutils::exists( db_path ) ) { if ( IOutils::exists( db_path ) ) {
// a database already exists, try rename it // a database already exists, try rename it
std::error_code err;
if ( ! IOutils::renameAsCopy( db_path, err ) ) { if ( ! IOutils::renameAsCopy( db_path, err ) ) {
// failed to rename // failed to rename
ok &= false; QString err_msg;
if ( err ) { if ( err ) {
err_msg = QString::fromStdString( err.message() ); err_msg = QString::fromStdString( err.message() );
} }
DialogSec::errRenaming( QString::fromStdString(db_path), err_msg ); DialogSec::errRenaming( QString::fromStdString(db_path), err_msg );
}/* else { return false;
// renamed successfully, make new one }
}*/
}
if ( ok ) {
ok = newCollectionDatabase( db, db_name, ws_names );
} }
return newCollectionDatabase( DatabaseHandler::get( DatabaseType::Data ), db_path, ws_names );
} catch (const LogDoctorException&) {
return false;
} }
if ( db.isOpen() ) { return true;
db.close();
}
return ok;
} }
bool checkHashesDatabase( const std::string& db_path ) noexcept bool checkHashesDatabase( const std::string& db_path ) noexcept
{ {
bool make_new{false}, ok{true};
std::error_code err;
QString err_msg;
const QString db_name{ QString::fromStdString( db_path.substr( db_path.find_last_of( '/' ) + 1 ) ) };
const std::vector<QString> ws_names { "apache", "nginx", "iis" }; const std::vector<QString> ws_names { "apache", "nginx", "iis" };
QSqlDatabase db{ QSqlDatabase::database(DatabasesNames::hashes) }; try {
db.setDatabaseName( QString::fromStdString( db_path ) );
// check the existence DatabaseWrapper db{ DatabaseHandler::get( DatabaseType::Hashes ) };
if ( IOutils::exists( db_path ) ) {
// check file type and permissions
if ( ! checkDatabaseFile( db_path, db_name ) ) {
ok &= false;
} else {
// database file seems ok, now try to open
if ( ! db.open() ) {
// error opening database
ok &= false;
DialogSec::errDatabaseFailedOpening( db_name, db.lastError().text() );
if ( ! IOutils::exists( db_path ) ) {
// database does not exist, yet, ask to create a new one
if ( DialogSec::choiceDatabaseNotFound( db.name() ) ) {
// choosed to create it
throw MakeNewDatabase{};
} else { } else {
// database successfully opened, now check the tables // refused to create it, abort
const int check = checkDatabaseTablesNames( db, db_name ); return false;
if ( check == 0 ) { }
ok &= false; }
} else if ( check == 2 ) {
make_new |= true;
}
QSqlQuery query{ QSqlQuery( db ) };
if ( ok && !make_new ) {
// check every WebServer table, both access and error // check file type and permissions
for ( const QString& table : ws_names ) { if ( ! checkDatabaseFile( db_path, db.name() ) ) {
return false;
}
if ( !ok || make_new ) { break; } // file seems ok, try to open
// column's name:type associations db.open( db_path, true );
bool name_ok{ false },
type_ok{ false };
// query table's columns' infoes for access logs // check the tables
if ( ! query.exec( "SELECT name, type FROM pragma_table_info('"+table+"') AS tbinfo;" ) ) { if ( ! checkDatabaseTablesNames( db.getQuery() ) ) {
// error opening database return false;
ok &= false; }
DialogSec::errDatabaseFailedExecuting( db_name, query.lastQuery(), query.lastError().text() );
}
// iterate over results
while ( query.next() ) {
const QString col_name{ query.value(0).toString() };
const QString col_type{ query.value(1).toString() };
if ( col_name != "hash" ) {
// unexpected column
if ( DialogSec::choiceDatabaseWrongColumn( db_name, table, col_name ) ) {
// agreed to renew
make_new |= true;
} else {
// refused to renew
ok &= false;
}
break;
} else { const QString stmt{ QStringLiteral("SELECT name, type FROM pragma_table_info('%1') AS tbinfo;") };
// column found, check the data-type
name_ok |= true; // check every WebServer table, both access and error
if ( col_type == "TEXT" ) { for ( const QString& table : ws_names ) {
// same data-type
type_ok |= true; QueryWrapper query{ db.getQuery() };
} else {
// different data-type, ask to renew query( stmt.arg( table ) );
if ( DialogSec::choiceDatabaseWrongDataType( db_name, table, col_name, col_type ) ) {
// agreed to renew while ( query->next() ) {
make_new |= true; const QString col_name{ query[0].toString() };
} else { const QString col_type{ query[1].toString() };
// refused to renew
ok &= false; if ( col_name != "hash" ) {
} // unexpected column
break; if ( DialogSec::choiceDatabaseWrongColumn( db.name(), table, col_name ) ) {
} // agreed to renew
} throw MakeNewDatabase{};
} } else {
if ( ok && !make_new ) { // refused to renew
if ( !name_ok || !type_ok ) { return false;
ok &= false; }
}
} } else if ( col_type != "TEXT" ) {
query.finish(); // different data-type, ask to renew
if ( DialogSec::choiceDatabaseWrongDataType( db.name(), table, col_name, col_type ) ) {
// agreed to renew
throw MakeNewDatabase{};
} else {
// refused to renew
return false;
} }
} }
} }
} }
} else { } catch (const MakeNewDatabase&) {
// database does not exist, yet, ask to create a new one
if ( DialogSec::choiceDatabaseNotFound( QString(db_name) ) ) {
// choosed to create it
make_new |= true;
} else {
// refused to create it, abort
ok &= false;
}
}
if ( ok && make_new ) {
// rename the current db file as a 'copy'
if ( IOutils::exists( db_path ) ) { if ( IOutils::exists( db_path ) ) {
// a database already exists, try rename it // a database already exists, try rename it
std::error_code err;
if ( ! IOutils::renameAsCopy( db_path, err ) ) { if ( ! IOutils::renameAsCopy( db_path, err ) ) {
// failed to rename // failed to rename
ok &= false; QString err_msg;
if ( err ) { if ( err ) {
err_msg = QString::fromStdString( err.message() ); err_msg = QString::fromStdString( err.message() );
} }
DialogSec::errRenaming( QString::fromStdString(db_path), err_msg ); DialogSec::errRenaming( QString::fromStdString(db_path), err_msg );
}/* else { return false;
// renamed successfully, make new one }
}*/
}
if ( ok ) {
ok = newHashesDatabase( db, db_name, ws_names );
} }
return newHashesDatabase( DatabaseHandler::get( DatabaseType::Hashes ), db_path, ws_names );
} catch (const LogDoctorException&) {
return false;
} }
if ( db.isOpen() ) { return true;
db.close();
}
return ok;
} }

View file

@ -92,58 +92,36 @@ QString printableTime( const unsigned seconds ) noexcept
{ {
const unsigned mins{ seconds / 60u }; const unsigned mins{ seconds / 60u };
const unsigned secs{ seconds - (mins*60u) }; const unsigned secs{ seconds - (mins*60u) };
return QString("%1:%2").arg( return QStringLiteral("%1:%2")
(mins<10u) .arg( mins, 2, 10, QChar('0') )
? QString("0%1").arg( mins ) .arg( secs, 2, 10, QChar('0') );
: QString::number( mins ),
(secs<10u)
? QString("0%1").arg( secs )
: QString::number( secs )
);
} }
QString printableTime( const int hour, const int minute, const int second ) noexcept QString printableTime( const int hour, const int minute, const int second ) noexcept
{ {
return QString("%1:%2:%3").arg( return QStringLiteral("%1:%2:%3")
(hour<10) .arg( hour, 2, 10, QChar('0') )
? QString("0%1").arg( hour ) .arg( minute, 2, 10, QChar('0') )
: QString::number( hour ), .arg( second, 2, 10, QChar('0') );
(minute<10)
? QString("0%1").arg( minute )
: QString::number( minute ),
(second<10)
? QString("0%1").arg( second )
: QString::number( second )
);
} }
QString printableDate( const QString& year, const int month, const QString& day ) noexcept QString printableDate( const QString& year, const int month, const QString& day ) noexcept
{ {
return QString("%1-%2-%3").arg( return QStringLiteral("%1-%2-%3")
year, .arg( year )
(month<10) .arg( month, 2, 10, QChar('0') )
? QString("0%1").arg( month ) .arg( day, 2, QChar('0') );
: QString::number( month ),
(day.size()<2)
? QString("0%1").arg( day )
: day
);
} }
QString printableDate( const int year, const int month, const int day ) noexcept QString printableDate( const int year, const int month, const int day ) noexcept
{ {
return QString("%1-%2-%3").arg( return QStringLiteral("%1-%2-%3")
QString::number( year ), .arg( year )
(month<10) .arg( month, 2, 10, QChar('0') )
? QString("0%1").arg( month ) .arg( day, 2, 10, QChar('0') );
: QString::number( month ),
(day<10)
? QString("0%1").arg( day )
: QString::number( day )
);
} }