Added streams save/restore; Ostinato file format specified; Protocol Field Numbering scheme changed - existing protocol field numbers changed accordingly

This commit is contained in:
Srivats P. 2010-07-13 20:36:35 +05:30
parent 2e6503faad
commit c2fac2db70
40 changed files with 1008 additions and 136 deletions

View File

@ -24,6 +24,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include <QFile>
#include <QSettings>
extern const char* version;
extern const char* revision;
extern ProtocolManager *OstProtocolManager;
QSettings *appSettings;
@ -34,6 +36,11 @@ int main(int argc, char* argv[])
QApplication app(argc, argv);
int exitCode;
app.setApplicationName("Ostinato");
app.setOrganizationName("Ostinato");
app.setProperty("version", version);
app.setProperty("revision", revision);
OstProtocolManager = new ProtocolManager();
/* (Portable Mode) If we have a .ini file in the same directory as the
@ -44,7 +51,7 @@ int main(int argc, char* argv[])
if (QFile::exists(portableIni))
appSettings = new QSettings(portableIni, QSettings::IniFormat);
else
appSettings = new QSettings("Ostinato", "Ostinato");
appSettings = new QSettings();
mainWindow = new MainWindow;
mainWindow->show();

View File

@ -17,12 +17,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include <vector>
#include <google/protobuf/descriptor.h>
#include "port.h"
#include "pbhelper.h"
#include "fileformat.h"
#include <QApplication>
#include <QVariant>
#include <google/protobuf/descriptor.h>
#include <vector>
uint Port::mAllocStreamId = 0;
@ -218,3 +220,43 @@ void Port::updateStats(OstProto::PortStats *portStats)
}
}
bool Port::openStreams(QString fileName, bool append, QString &error)
{
OstProto::StreamConfigList streams;
if (!fileFormat.openStreams(fileName, streams, error))
goto _fail;
if (!append)
{
while (numStreams())
deleteStreamAt(0);
}
for (int i = 0; i < streams.stream_size(); i++)
{
newStreamAt(mStreams.size());
streamByIndex(mStreams.size()-1)->protoDataCopyFrom(streams.stream(i));
}
emit streamListChanged(mPortGroupId, mPortId);
return true;
_fail:
return false;
}
bool Port::saveStreams(QString fileName, QString &error)
{
OstProto::StreamConfigList streams;
streams.mutable_port_id()->set_id(0);
for (int i = 0; i < mStreams.size(); i++)
{
OstProto::Stream *s = streams.add_stream();
mStreams[i]->protoDataCopyInto(*s);
}
return fileFormat.saveStreams(streams, fileName, error);
}

View File

@ -114,13 +114,16 @@ public:
void getModifiedStreamsSinceLastSync(
OstProto::StreamConfigList &streamConfigList);
void when_syncComplete();
void updateStats(OstProto::PortStats *portStats);
bool openStreams(QString fileName, bool append, QString &error);
bool saveStreams(QString fileName, QString &error);
signals:
void portDataChanged(int portGroupId, int portId);
void streamListChanged(int portGroupId, int portId);
};

View File

@ -29,6 +29,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent)
: QWidget(parent)
{
QAction *sep;
delegate = new StreamListDelegate;
//slm = new StreamListModel();
//plm = new PortGroupList();
@ -43,7 +45,7 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent)
tvStreamList->verticalHeader()->setDefaultSectionSize(
tvStreamList->verticalHeader()->minimumSectionSize());
// Populate Context Menu Actions
// Populate PortList Context Menu Actions
tvPortList->addAction(actionNew_Port_Group);
tvPortList->addAction(actionDelete_Port_Group);
tvPortList->addAction(actionConnect_Port_Group);
@ -51,12 +53,21 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent)
tvPortList->addAction(actionExclusive_Control);
// Populate StramList Context Menu Actions
tvStreamList->addAction(actionNew_Stream);
tvStreamList->addAction(actionEdit_Stream);
tvStreamList->addAction(actionDelete_Stream);
sep = new QAction(this);
sep->setSeparator(true);
tvStreamList->addAction(sep);
tvStreamList->addAction(actionOpen_Streams);
tvStreamList->addAction(actionSave_Streams);
// PortList and StreamList actions combined make this window's actions
addActions(tvPortList->actions());
QAction *sep = new QAction(this);
sep = new QAction(this);
sep->setSeparator(true);
addAction(sep);
addActions(tvStreamList->actions());
@ -77,26 +88,24 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent)
this, SLOT(when_portView_currentChanged(const QModelIndex&,
const QModelIndex&)));
connect( tvStreamList->selectionModel(),
SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
this, SLOT(when_streamView_currentChanged(const QModelIndex&,
const QModelIndex&)));
connect( tvStreamList->selectionModel(),
SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
this, SLOT(when_streamView_selectionChanged()));
connect(plm->getStreamModel(), SIGNAL(rowsInserted(QModelIndex, int, int)),
SLOT(updateStreamViewActions()));
connect(plm->getStreamModel(), SIGNAL(rowsRemoved(QModelIndex, int, int)),
SLOT(updateStreamViewActions()));
#if 0
connect( tvPortList->selectionModel(),
connect(tvStreamList->selectionModel(),
SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
plm->getStreamModel(), SLOT(setCurrentPortIndex(const QModelIndex&)));
#endif
SLOT(updateStreamViewActions()));
connect(tvStreamList->selectionModel(),
SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
SLOT(updateStreamViewActions()));
tvStreamList->resizeColumnToContents(StreamModel::StreamIcon);
tvStreamList->resizeColumnToContents(StreamModel::StreamStatus);
// Initially we don't have any ports/streams - so send signal triggers
when_portView_currentChanged(QModelIndex(), QModelIndex());
when_streamView_currentChanged(QModelIndex(), QModelIndex());
updateStreamViewActions();
//! \todo Hide the Aggregate Box till we add support
frAggregate->setHidden(true);
@ -148,19 +157,6 @@ void PortsWindow::when_portView_currentChanged(const QModelIndex& current,
}
}
void PortsWindow::when_streamView_currentChanged(const QModelIndex& /*current*/,
const QModelIndex& /*previous*/)
{
qDebug("stream view current changed");
updateStreamViewActions();
}
void PortsWindow::when_streamView_selectionChanged()
{
qDebug("stream view selection changed");
updateStreamViewActions();
}
void PortsWindow::when_portModel_dataChanged(const QModelIndex& topLeft,
const QModelIndex& bottomRight)
{
@ -182,16 +178,6 @@ void PortsWindow::when_portModel_reset()
when_portView_currentChanged(QModelIndex(), tvPortList->currentIndex());
}
#if 0
void PortsWindow::updateStreamViewActions(const QModelIndex& current)
{
if (current.isValid())
actionDelete_Stream->setEnabled(true);
else
actionDelete_Stream->setDisabled(true);
}
#endif
void PortsWindow::updateStreamViewActions()
{
// For some reason hasSelection() returns true even if selection size is 0
@ -233,6 +219,9 @@ void PortsWindow::updateStreamViewActions()
actionEdit_Stream->setDisabled(true);
actionDelete_Stream->setDisabled(true);
}
actionOpen_Streams->setEnabled(plm->isPort(
tvPortList->selectionModel()->currentIndex()));
actionSave_Streams->setEnabled(tvStreamList->model()->rowCount() > 0);
}
void PortsWindow::updatePortViewActions(const QModelIndex& current)
@ -406,26 +395,6 @@ void PortsWindow::on_actionExclusive_Control_triggered(bool checked)
plm->portGroup(current.parent()).modifyPort(current.row(), checked);
}
#if 0
void PortsWindow::on_actionNew_Stream_triggered()
{
qDebug("New Stream Action");
int row = 0;
if (tvStreamList->currentIndex().isValid())
row = tvStreamList->currentIndex().row();
plm->getStreamModel()->insertRows(row, 1);
}
void PortsWindow::on_actionDelete_Stream_triggered()
{
qDebug("Delete Stream Action");
if (tvStreamList->currentIndex().isValid())
plm->getStreamModel()->removeRows(tvStreamList->currentIndex().row(), 1);
}
#endif
void PortsWindow::on_actionNew_Stream_triggered()
{
qDebug("New Stream Action");
@ -477,4 +446,73 @@ void PortsWindow::on_actionDelete_Stream_triggered()
qDebug("No selection");
}
void PortsWindow::on_actionOpen_Streams_triggered()
{
qDebug("Open Streams Action");
QModelIndex current = tvPortList->selectionModel()->currentIndex();
QString fileName;
QString errorStr;
bool append = true;
Q_ASSERT(plm->isPort(current));
fileName = QFileDialog::getOpenFileName(this, tr("Open Streams"));
if (fileName.isEmpty())
goto _exit;
if (tvStreamList->model()->rowCount())
{
QMessageBox msgBox(QMessageBox::Question, qApp->applicationName(),
tr("Append to existing streams? Or overwrite?"));
QPushButton *appendBtn = msgBox.addButton(tr("Append"),
QMessageBox::ActionRole);
QPushButton *overwriteBtn = msgBox.addButton(tr("Overwrite"),
QMessageBox::ActionRole);
QPushButton *cancelBtn = msgBox.addButton(QMessageBox::Cancel);
msgBox.exec();
if (msgBox.clickedButton() == cancelBtn)
goto _exit;
else if (msgBox.clickedButton() == appendBtn)
append = true;
else if (msgBox.clickedButton() == overwriteBtn)
append = false;
else
Q_ASSERT(false);
}
if (!plm->port(current).openStreams(fileName, append, errorStr))
QMessageBox::critical(this, qApp->applicationName(), errorStr);
else if (!errorStr.isEmpty())
QMessageBox::warning(this, qApp->applicationName(), errorStr);
_exit:
return;
}
void PortsWindow::on_actionSave_Streams_triggered()
{
qDebug("Save Streams Action");
QModelIndex current = tvPortList->selectionModel()->currentIndex();
QString fileName;
QString errorStr;
Q_ASSERT(plm->isPort(current));
fileName = QFileDialog::getSaveFileName(this, tr("Save Streams"));
if (fileName.isEmpty())
goto _exit;
// TODO: all or selected?
if (!plm->port(current).saveStreams(fileName, errorStr))
QMessageBox::critical(this, qApp->applicationName(), errorStr);
else if (!errorStr.isEmpty())
QMessageBox::warning(this, qApp->applicationName(), errorStr);
_exit:
return;
}

View File

@ -48,17 +48,13 @@ private:
QString lastNewPortGroup;
QAbstractItemDelegate *delegate;
private slots:
void updatePortViewActions(const QModelIndex& current);
//void updateStreamViewActions(const QModelIndex& current);
void updateStreamViewActions();
private slots:
void on_tvStreamList_activated(const QModelIndex & index);
void when_portView_currentChanged(const QModelIndex& current,
const QModelIndex& previous);
void when_streamView_currentChanged(const QModelIndex& current,
const QModelIndex& previous);
void when_streamView_selectionChanged();
void when_portModel_dataChanged(const QModelIndex& topLeft,
const QModelIndex& bottomRight);
void when_portModel_reset();
@ -75,6 +71,9 @@ private slots:
void on_actionNew_Stream_triggered();
void on_actionEdit_Stream_triggered();
void on_actionDelete_Stream_triggered();
void on_actionOpen_Streams_triggered();
void on_actionSave_Streams_triggered();
};
#endif

View File

@ -230,6 +230,16 @@
<string>Exclusive Port Control (EXPERIMENTAL)</string>
</property>
</action>
<action name="actionOpen_Streams" >
<property name="text" >
<string>Open Streams ...</string>
</property>
</action>
<action name="actionSave_Streams" >
<property name="text" >
<string>Save Streams ...</string>
</property>
</action>
</widget>
<resources>
<include location="ostinato.qrc" />

View File

@ -254,8 +254,30 @@ void StreamModel::setCurrentPortIndex(const QModelIndex &current)
else
{
qDebug("change to valid port");
// Disconnect any existing connection to avoid duplication
// Qt 4.6 has Qt::UniqueConnection, but we want to remain compatible
// with earlier Qt versions
if (mCurrentPort)
{
disconnect(mCurrentPort, SIGNAL(streamListChanged(int, int)),
this, SLOT(when_mCurrentPort_streamListChanged(int, int)));
}
quint16 pg = current.internalId() >> 16;
mCurrentPort = pgl->mPortGroups[pgl->indexOfPortGroup(pg)]->mPorts[current.row()];
connect(mCurrentPort, SIGNAL(streamListChanged(int, int)),
this, SLOT(when_mCurrentPort_streamListChanged(int, int)));
}
reset();
}
void StreamModel::when_mCurrentPort_streamListChanged(int portGroupId,
int portId)
{
qDebug("In %s", __FUNCTION__);
if (mCurrentPort)
{
if ((quint32(portGroupId) == mCurrentPort->portGroupId())
&& (quint32(portId) == mCurrentPort->id()))
reset();
}
}

View File

@ -71,6 +71,8 @@ class StreamModel : public QAbstractTableModel
public slots:
void setCurrentPortIndex(const QModelIndex &current);
private slots:
void when_mCurrentPort_streamListChanged(int portGroupId, int portId);
};
#endif

View File

@ -63,5 +63,5 @@ message Arp {
}
extend Protocol {
optional Arp arp = 130;
optional Arp arp = 300;
}

134
common/crc32c.cpp Normal file
View File

@ -0,0 +1,134 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
/*******************************************************************
** IMPORTANT NOTE:
** This code is from RFC 4960 Stream Control Transmission Protocol
** It has been modified suitably while keeping the algorithm intact.
********************************************************************/
#include "crc32c.h"
#define CRC32C(c,d) (c=(c>>8)^crc_c[(c^(d))&0xFF])
quint32 crc_c[256] =
{
0x00000000L, 0xF26B8303L, 0xE13B70F7L, 0x1350F3F4L,
0xC79A971FL, 0x35F1141CL, 0x26A1E7E8L, 0xD4CA64EBL,
0x8AD958CFL, 0x78B2DBCCL, 0x6BE22838L, 0x9989AB3BL,
0x4D43CFD0L, 0xBF284CD3L, 0xAC78BF27L, 0x5E133C24L,
0x105EC76FL, 0xE235446CL, 0xF165B798L, 0x030E349BL,
0xD7C45070L, 0x25AFD373L, 0x36FF2087L, 0xC494A384L,
0x9A879FA0L, 0x68EC1CA3L, 0x7BBCEF57L, 0x89D76C54L,
0x5D1D08BFL, 0xAF768BBCL, 0xBC267848L, 0x4E4DFB4BL,
0x20BD8EDEL, 0xD2D60DDDL, 0xC186FE29L, 0x33ED7D2AL,
0xE72719C1L, 0x154C9AC2L, 0x061C6936L, 0xF477EA35L,
0xAA64D611L, 0x580F5512L, 0x4B5FA6E6L, 0xB93425E5L,
0x6DFE410EL, 0x9F95C20DL, 0x8CC531F9L, 0x7EAEB2FAL,
0x30E349B1L, 0xC288CAB2L, 0xD1D83946L, 0x23B3BA45L,
0xF779DEAEL, 0x05125DADL, 0x1642AE59L, 0xE4292D5AL,
0xBA3A117EL, 0x4851927DL, 0x5B016189L, 0xA96AE28AL,
0x7DA08661L, 0x8FCB0562L, 0x9C9BF696L, 0x6EF07595L,
0x417B1DBCL, 0xB3109EBFL, 0xA0406D4BL, 0x522BEE48L,
0x86E18AA3L, 0x748A09A0L, 0x67DAFA54L, 0x95B17957L,
0xCBA24573L, 0x39C9C670L, 0x2A993584L, 0xD8F2B687L,
0x0C38D26CL, 0xFE53516FL, 0xED03A29BL, 0x1F682198L,
0x5125DAD3L, 0xA34E59D0L, 0xB01EAA24L, 0x42752927L,
0x96BF4DCCL, 0x64D4CECFL, 0x77843D3BL, 0x85EFBE38L,
0xDBFC821CL, 0x2997011FL, 0x3AC7F2EBL, 0xC8AC71E8L,
0x1C661503L, 0xEE0D9600L, 0xFD5D65F4L, 0x0F36E6F7L,
0x61C69362L, 0x93AD1061L, 0x80FDE395L, 0x72966096L,
0xA65C047DL, 0x5437877EL, 0x4767748AL, 0xB50CF789L,
0xEB1FCBADL, 0x197448AEL, 0x0A24BB5AL, 0xF84F3859L,
0x2C855CB2L, 0xDEEEDFB1L, 0xCDBE2C45L, 0x3FD5AF46L,
0x7198540DL, 0x83F3D70EL, 0x90A324FAL, 0x62C8A7F9L,
0xB602C312L, 0x44694011L, 0x5739B3E5L, 0xA55230E6L,
0xFB410CC2L, 0x092A8FC1L, 0x1A7A7C35L, 0xE811FF36L,
0x3CDB9BDDL, 0xCEB018DEL, 0xDDE0EB2AL, 0x2F8B6829L,
0x82F63B78L, 0x709DB87BL, 0x63CD4B8FL, 0x91A6C88CL,
0x456CAC67L, 0xB7072F64L, 0xA457DC90L, 0x563C5F93L,
0x082F63B7L, 0xFA44E0B4L, 0xE9141340L, 0x1B7F9043L,
0xCFB5F4A8L, 0x3DDE77ABL, 0x2E8E845FL, 0xDCE5075CL,
0x92A8FC17L, 0x60C37F14L, 0x73938CE0L, 0x81F80FE3L,
0x55326B08L, 0xA759E80BL, 0xB4091BFFL, 0x466298FCL,
0x1871A4D8L, 0xEA1A27DBL, 0xF94AD42FL, 0x0B21572CL,
0xDFEB33C7L, 0x2D80B0C4L, 0x3ED04330L, 0xCCBBC033L,
0xA24BB5A6L, 0x502036A5L, 0x4370C551L, 0xB11B4652L,
0x65D122B9L, 0x97BAA1BAL, 0x84EA524EL, 0x7681D14DL,
0x2892ED69L, 0xDAF96E6AL, 0xC9A99D9EL, 0x3BC21E9DL,
0xEF087A76L, 0x1D63F975L, 0x0E330A81L, 0xFC588982L,
0xB21572C9L, 0x407EF1CAL, 0x532E023EL, 0xA145813DL,
0x758FE5D6L, 0x87E466D5L, 0x94B49521L, 0x66DF1622L,
0x38CC2A06L, 0xCAA7A905L, 0xD9F75AF1L, 0x2B9CD9F2L,
0xFF56BD19L, 0x0D3D3E1AL, 0x1E6DCDEEL, 0xEC064EEDL,
0xC38D26C4L, 0x31E6A5C7L, 0x22B65633L, 0xD0DDD530L,
0x0417B1DBL, 0xF67C32D8L, 0xE52CC12CL, 0x1747422FL,
0x49547E0BL, 0xBB3FFD08L, 0xA86F0EFCL, 0x5A048DFFL,
0x8ECEE914L, 0x7CA56A17L, 0x6FF599E3L, 0x9D9E1AE0L,
0xD3D3E1ABL, 0x21B862A8L, 0x32E8915CL, 0xC083125FL,
0x144976B4L, 0xE622F5B7L, 0xF5720643L, 0x07198540L,
0x590AB964L, 0xAB613A67L, 0xB831C993L, 0x4A5A4A90L,
0x9E902E7BL, 0x6CFBAD78L, 0x7FAB5E8CL, 0x8DC0DD8FL,
0xE330A81AL, 0x115B2B19L, 0x020BD8EDL, 0xF0605BEEL,
0x24AA3F05L, 0xD6C1BC06L, 0xC5914FF2L, 0x37FACCF1L,
0x69E9F0D5L, 0x9B8273D6L, 0x88D28022L, 0x7AB90321L,
0xAE7367CAL, 0x5C18E4C9L, 0x4F48173DL, 0xBD23943EL,
0xF36E6F75L, 0x0105EC76L, 0x12551F82L, 0xE03E9C81L,
0x34F4F86AL, 0xC69F7B69L, 0xD5CF889DL, 0x27A40B9EL,
0x79B737BAL, 0x8BDCB4B9L, 0x988C474DL, 0x6AE7C44EL,
0xBE2DA0A5L, 0x4C4623A6L, 0x5F16D052L, 0xAD7D5351L,
};
quint32 checksumCrc32C(quint8 *buffer, uint length)
{
uint i;
quint32 crc32 = ~0L;
quint32 result;
quint8 byte0,byte1,byte2,byte3;
for (i = 0; i < length; i++) {
CRC32C(crc32, buffer[i]);
}
result = ~crc32;
/* result now holds the negated polynomial remainder;
* since the table and algorithm is "reflected" [williams95].
* That is, result has the same value as if we mapped the message
* to a polynomial, computed the host-bit-order polynomial
* remainder, performed final negation, then did an end-for-end
* bit-reversal.
* Note that a 32-bit bit-reversal is identical to four inplace
* 8-bit reversals followed by an end-for-end byteswap.
* In other words, the bytes of each bit are in the right order,
* but the bytes have been byteswapped. So we now do an explicit
* byteswap. On a little-endian machine, this byteswap and
* the final ntohl cancel out and could be elided.
*/
byte0 = result & 0xff;
byte1 = (result>>8) & 0xff;
byte2 = (result>>16) & 0xff;
byte3 = (result>>24) & 0xff;
crc32 = ((byte0 << 24) |
(byte1 << 16) |
(byte2 << 8) |
byte3);
return ( crc32 );
}

23
common/crc32c.h Normal file
View File

@ -0,0 +1,23 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include <QtGlobal>
quint32 checksumCrc32C(quint8 *buffer, uint length);

View File

@ -29,5 +29,5 @@ message Dot2Llc {
}
extend Protocol {
optional Dot2Llc dot2Llc = 127;
optional Dot2Llc dot2Llc = 206;
}

View File

@ -27,5 +27,5 @@ message Dot2Snap {
}
extend Protocol {
optional Dot2Snap dot2Snap = 128;
optional Dot2Snap dot2Snap = 207;
}

View File

@ -27,5 +27,5 @@ message Dot3 {
}
extend Protocol {
optional Dot3 dot3 = 122;
optional Dot3 dot3 = 201;
}

View File

@ -27,5 +27,5 @@ message Eth2 {
}
extend Protocol {
optional Eth2 eth2 = 121;
optional Eth2 eth2 = 200;
}

411
common/fileformat.cpp Normal file
View File

@ -0,0 +1,411 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "fileformat.h"
#include "crc32c.h"
#include <QApplication>
#include <QFile>
#include <QVariant>
#include <string>
const std::string FileFormat::kFileMagicValue = "\xa7\xb7OSTINATO";
FileFormat fileFormat;
const int kBaseHex = 16;
FileFormat::FileFormat()
{
/*
* We don't have any "real" work to do here in the constructor.
* What we do is run some "assert" tests so that these get caught
* at init itself instead of while saving/restoring when a user
* might lose some data!
*/
OstProto::FileMagic magic;
OstProto::FileChecksum cksum;
magic.set_value(kFileMagicValue);
cksum.set_value(quint32(0));
// TODO: convert Q_ASSERT to something that will run in RELEASE mode also
Q_ASSERT(magic.IsInitialized());
Q_ASSERT(cksum.IsInitialized());
Q_ASSERT(magic.ByteSize() == kFileMagicSize);
Q_ASSERT(cksum.ByteSize() == kFileChecksumSize);
}
FileFormat::~FileFormat()
{
}
bool FileFormat::openStreams(const QString fileName,
OstProto::StreamConfigList &streams, QString &error)
{
QFile file(fileName);
QByteArray buf;
int size, contentOffset, contentSize;
quint32 calcCksum;
OstProto::FileMagic magic;
OstProto::FileMeta meta;
OstProto::FileContent content;
OstProto::FileChecksum cksum, zeroCksum;
if (!file.open(QIODevice::ReadOnly))
goto _open_fail;
if (file.size() < kFileMagicSize)
goto _magic_missing;
if (file.size() < kFileMinSize)
goto _checksum_missing;
buf.resize(file.size());
size = file.read(buf.data(), buf.size());
if (size < 0)
goto _read_fail;
Q_ASSERT(file.atEnd());
file.close();
qDebug("%s: file.size() = %lld", __FUNCTION__, file.size());
qDebug("%s: size = %d", __FUNCTION__, size);
//qDebug("Read %d bytes", buf.size());
//qDebug("%s", QString(buf.toHex()).toAscii().constData());
// Parse and verify magic
if (!magic.ParseFromArray(
(void*)(buf.constData() + kFileMagicOffset),
kFileMagicSize))
{
goto _magic_parse_fail;
}
if (magic.value() != kFileMagicValue)
goto _magic_match_fail;
// Parse and verify checksum
if (!cksum.ParseFromArray(
(void*)(buf.constData() + size - kFileChecksumSize),
kFileChecksumSize))
{
goto _cksum_parse_fail;
}
zeroCksum.set_value(0);
if (!zeroCksum.SerializeToArray(
(void*) (buf.data() + size - kFileChecksumSize),
kFileChecksumSize))
{
goto _zero_cksum_serialize_fail;
}
calcCksum = checksumCrc32C((quint8*) buf.constData(), size);
qDebug("checksum \nExpected:%x Actual:%x",
calcCksum, cksum.value());
if (cksum.value() != calcCksum)
goto _cksum_verify_fail;
// Parse the metadata first before we parse the full contents
if (!meta.ParseFromArray(
(void*)(buf.constData() + kFileMetaDataOffset),
size - kFileMetaDataOffset))
{
goto _metadata_parse_fail;
}
qDebug("%s: File MetaData (INFORMATION) - \n%s", __FUNCTION__,
QString().fromStdString(meta.DebugString()).toAscii().constData());
// MetaData Validation(s)
if (meta.data().file_type() != OstProto::kStreamsFileType)
goto _unexpected_file_type;
if (meta.data().format_version_major() != kFileFormatVersionMajor)
goto _incompatible_file_version;
if (meta.data().format_version_minor() > kFileFormatVersionMinor)
goto _incompatible_file_version;
if (meta.data().format_version_minor() < kFileFormatVersionMinor)
{
// TODO: need to modify 'buf' such that we can parse successfully
// assuming the native minor version
}
if (meta.data().format_version_revision() > kFileFormatVersionRevision)
{
error = QString(tr("%1 was created using a newer version of Ostinato."
" New features/protocols will not be available.")).arg(fileName);
}
Q_ASSERT(meta.data().format_version_major() == kFileFormatVersionMajor);
Q_ASSERT(meta.data().format_version_minor() == kFileFormatVersionMinor);
// ByteSize() does not include the Tag/Key, so we add 2 for that
contentOffset = kFileMetaDataOffset + meta.data().ByteSize() + 2;
contentSize = size - contentOffset - kFileChecksumSize;
// Parse full contents
if (!content.ParseFromArray(
(void*)(buf.constData() + contentOffset),
contentSize))
{
goto _content_parse_fail;
}
if (!content.matter().has_streams())
goto _missing_streams;
streams.CopyFrom(content.matter().streams());
return true;
_missing_streams:
error = QString(tr("%1 does not contain any streams")).arg(fileName);
goto _fail;
_content_parse_fail:
error = QString(tr("Failed parsing %1 contents")).arg(fileName);
qDebug("Error: %s", QString().fromStdString(
content.matter().InitializationErrorString())
.toAscii().constData());
qDebug("Debug: %s", QString().fromStdString(
content.matter().DebugString()).toAscii().constData());
goto _fail;
_incompatible_file_version:
error = QString(tr("%1 is in an incompatible format version - %2.%3.%4"
" (Native version is %5.%6.%7)"))
.arg(fileName)
.arg(meta.data().format_version_major())
.arg(meta.data().format_version_minor())
.arg(meta.data().format_version_revision())
.arg(kFileFormatVersionMajor)
.arg(kFileFormatVersionMinor)
.arg(kFileFormatVersionRevision);
goto _fail;
_unexpected_file_type:
error = QString(tr("%1 is not a streams file")).arg(fileName);
goto _fail;
_metadata_parse_fail:
error = QString(tr("Failed parsing %1 meta data")).arg(fileName);
qDebug("Error: %s", QString().fromStdString(
meta.data().InitializationErrorString())
.toAscii().constData());
goto _fail;
_cksum_verify_fail:
error = QString(tr("%1 checksum validation failed!\nExpected:%2 Actual:%3"))
.arg(fileName)
.arg(calcCksum, 0, kBaseHex)
.arg(cksum.value(), 0, kBaseHex);
goto _fail;
_zero_cksum_serialize_fail:
error = QString(tr("Internal Error: Zero Checksum Serialize failed!\n"
"Error: %1\nDebug: %2"))
.arg(QString().fromStdString(
cksum.InitializationErrorString()))
.arg(QString().fromStdString(cksum.DebugString()));
goto _fail;
_cksum_parse_fail:
error = QString(tr("Failed parsing %1 checksum")).arg(fileName);
qDebug("Error: %s", QString().fromStdString(
cksum.InitializationErrorString())
.toAscii().constData());
goto _fail;
_magic_match_fail:
error = QString(tr("%1 is not an Ostinato file")).arg(fileName);
goto _fail;
_magic_parse_fail:
error = QString(tr("%1 does not look like an Ostinato file")).arg(fileName);
qDebug("Error: %s", QString().fromStdString(
magic.InitializationErrorString())
.toAscii().constData());
goto _fail;
_read_fail:
error = QString(tr("Error reading from %1")).arg(fileName);
goto _fail;
_checksum_missing:
error = QString(tr("%1 is too small (missing checksum)")).arg(fileName);
goto _fail;
_magic_missing:
error = QString(tr("%1 is too small (missing magic value)"))
.arg(fileName);
goto _fail;
_open_fail:
error = QString(tr("Error opening %1")).arg(fileName);
goto _fail;
_fail:
qDebug("%s", error.toAscii().constData());
return false;
}
bool FileFormat::saveStreams(const OstProto::StreamConfigList streams,
const QString fileName, QString &error)
{
OstProto::FileMagic magic;
OstProto::FileMeta meta;
OstProto::FileContent content;
OstProto::FileChecksum cksum;
QFile file(fileName);
int metaSize, contentSize;
int contentOffset, cksumOffset;
QByteArray buf;
quint32 calcCksum;
magic.set_value(kFileMagicValue);
Q_ASSERT(magic.IsInitialized());
cksum.set_value(0);
Q_ASSERT(cksum.IsInitialized());
initFileMetaData(*(meta.mutable_data()));
meta.mutable_data()->set_file_type(OstProto::kStreamsFileType);
Q_ASSERT(meta.IsInitialized());
if (!streams.IsInitialized())
goto _stream_not_init;
content.mutable_matter()->mutable_streams()->CopyFrom(streams);
Q_ASSERT(content.IsInitialized());
metaSize = meta.ByteSize();
contentSize = content.ByteSize();
contentOffset = kFileMetaDataOffset + metaSize;
cksumOffset = contentOffset + contentSize;
Q_ASSERT(magic.ByteSize() == kFileMagicSize);
Q_ASSERT(cksum.ByteSize() == kFileChecksumSize);
buf.resize(kFileMagicSize + metaSize + contentSize + kFileChecksumSize);
// Serialize everything
if (!magic.SerializeToArray((void*) (buf.data() + kFileMagicOffset),
kFileMagicSize))
{
goto _magic_serialize_fail;
}
if (!meta.SerializeToArray((void*) (buf.data() + kFileMetaDataOffset),
metaSize))
{
goto _meta_serialize_fail;
}
if (!content.SerializeToArray((void*) (buf.data() + contentOffset),
contentSize))
{
goto _content_serialize_fail;
}
if (!cksum.SerializeToArray((void*) (buf.data() + cksumOffset),
kFileChecksumSize))
{
goto _zero_cksum_serialize_fail;
}
// Calculate and write checksum
calcCksum = checksumCrc32C((quint8*)buf.constData(), buf.size());
cksum.set_value(calcCksum);
if (!cksum.SerializeToArray(
(void*) (buf.data() + cksumOffset),
kFileChecksumSize))
{
goto _cksum_serialize_fail;
}
qDebug("Writing %d bytes", buf.size());
//qDebug("%s", QString(buf.toHex()).toAscii().constData());
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
goto _open_fail;
if (file.write(buf) < 0)
goto _write_fail;
file.close();
return true;
_write_fail:
error = QString(tr("Error writing to %1")).arg(fileName);
goto _fail;
_open_fail:
error = QString(tr("Error opening %1 (Error Code = %2)"))
.arg(fileName)
.arg(file.error());
goto _fail;
_cksum_serialize_fail:
error = QString(tr("Internal Error: Checksum Serialize failed\n%1\n%2"))
.arg(QString().fromStdString(
cksum.InitializationErrorString()))
.arg(QString().fromStdString(cksum.DebugString()));
goto _fail;
_zero_cksum_serialize_fail:
error = QString(tr("Internal Eror: Zero Checksum Serialize failed\n%1\n%2"))
.arg(QString().fromStdString(
cksum.InitializationErrorString()))
.arg(QString().fromStdString(cksum.DebugString()));
goto _fail;
_content_serialize_fail:
error = QString(tr("Internal Error: Content Serialize failed\n%1\n%2"))
.arg(QString().fromStdString(
content.InitializationErrorString()))
.arg(QString().fromStdString(content.DebugString()));
goto _fail;
_meta_serialize_fail:
error = QString(tr("Internal Error: Meta Data Serialize failed\n%1\n%2"))
.arg(QString().fromStdString(
meta.InitializationErrorString()))
.arg(QString().fromStdString(meta.DebugString()));
goto _fail;
_magic_serialize_fail:
error = QString(tr("Internal Error: Magic Serialize failed\n%1\n%2"))
.arg(QString().fromStdString(
magic.InitializationErrorString()))
.arg(QString().fromStdString(magic.DebugString()));
goto _fail;
_stream_not_init:
error = QString(tr("Internal Error: Streams not initialized\n%1\n%2"))
.arg(QString().fromStdString(
streams.InitializationErrorString()))
.arg(QString().fromStdString(streams.DebugString()));
goto _fail;
_fail:
qDebug("%s", error.toAscii().constData());
return false;
}
void FileFormat::initFileMetaData(OstProto::FileMetaData &metaData)
{
// Fill in the "native" file format version
metaData.set_format_version_major(kFileFormatVersionMajor);
metaData.set_format_version_minor(kFileFormatVersionMinor);
metaData.set_format_version_revision(kFileFormatVersionRevision);
metaData.set_generator_name(
qApp->applicationName().toUtf8().constData());
metaData.set_generator_version(
qApp->property("version").toString().toUtf8().constData());
metaData.set_generator_revision(
qApp->property("revision").toString().toUtf8().constData());
}

59
common/fileformat.h Normal file
View File

@ -0,0 +1,59 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _FILE_FORMAT_H
#define _FILE_FORMAT_H
#include "fileformat.pb.h"
#include <QString>
#include <QObject>
class FileFormat : public QObject
{
Q_OBJECT
public:
FileFormat();
~FileFormat();
bool openStreams(const QString fileName,
OstProto::StreamConfigList &streams, QString &error);
bool saveStreams(const OstProto::StreamConfigList streams,
const QString fileName, QString &error);
private:
static const int kFileMagicSize = 12;
static const int kFileChecksumSize = 5;
static const int kFileMinSize = kFileMagicSize + kFileChecksumSize;
static const int kFileMagicOffset = 0;
static const int kFileMetaDataOffset = kFileMagicSize;
static const std::string kFileMagicValue;
// Native file format version
static const uint kFileFormatVersionMajor = 0;
static const uint kFileFormatVersionMinor = 1;
static const uint kFileFormatVersionRevision = 0;
void initFileMetaData(OstProto::FileMetaData &metaData);
};
extern FileFormat fileFormat;
#endif

98
common/fileformat.proto Normal file
View File

@ -0,0 +1,98 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
import "protocol.proto";
package OstProto;
enum FileType {
kReservedFileType = 0;
kStreamsFileType = 1;
}
message FileMetaData {
required FileType file_type = 1;
required uint32 format_version_major = 2;
required uint32 format_version_minor = 3;
required uint32 format_version_revision = 4;
required string generator_name = 5;
required string generator_version = 6;
required string generator_revision = 7;
}
message FileContentMatter {
optional StreamConfigList streams = 1;
}
/*
An Ostinato file is the binary encoding of the File message below
STRICTLY in increasing order of field number for the top level fields
We do not use field number '1' for magic value because its encoded key
is '0a' (LF or \n) which occurs commonly in text files. Checksum should
be the last field, so top level field numbers greater than 15 are not
permitted. We use 15 as the checksum field number because it is the
largest field number that can fit in a 1-byte tag
The magic value is of a fixed length so that meta data has a fixed offset
from the start in the encoded message.
Checksum is fixed length so that it is at a fixed negative offset from
the end in the encoded message.
Because the protobuf serialization API does not _guarantee_
strict ordering, so we define wrapper messages for each top level field
and serialize the individual wrapper messages. The field numbers MUST
be the same in 'File' and the wrapper messages
*/
message File {
// FieldNumber 1 is reserved and MUST not be used!
required bytes magic_value = 2;
required FileMetaData meta_data = 3;
optional FileContent content_matter = 9;
required fixed32 checksum_value = 15;
}
/*
The magic value is 10 bytes - "\xa7\xb7OSTINATO"
The 1st-2nd byte has the MSB set to avoid mixup with text files
The 3rd-10th byte spell OSTINATO (duh!)
Encoded Size : Key(1) + Length(1) + Value(10) = 12 bytes
Encoded Value: 120aa7b7 4f535449 4e41544f
*/
message FileMagic {
required bytes value = 2;
}
message FileMeta {
required FileMetaData data = 3;
}
message FileContent {
optional FileContentMatter matter = 9;
}
/*
Encoded Size : Key(1) + Value(4) = 5 bytes
Encoded Value: 7d xxXXxxXX
*/
message FileChecksum {
required fixed32 value = 15; // should always be a fixed 32-bit size
}

View File

@ -40,5 +40,5 @@ message Icmp {
}
extend Protocol {
optional Icmp icmp = 142;
optional Icmp icmp = 402;
}

View File

@ -61,5 +61,5 @@ message Ip4 {
}
extend Protocol {
optional Ip4 ip4 = 131;
optional Ip4 ip4 = 301;
}

View File

@ -33,5 +33,5 @@ extend Ip4over4 {
}
extend Protocol {
optional Ip4over4 ip4over4 = 135;
optional Ip4over4 ip4over4 = 305;
}

View File

@ -27,5 +27,5 @@ message Ip4over6 {
}
extend Protocol {
optional Ip4over6 ip4over6 = 134;
optional Ip4over6 ip4over6 = 304;
}

View File

@ -57,5 +57,5 @@ message Ip6 {
}
extend Protocol {
optional Ip6 ip6 = 132;
optional Ip6 ip6 = 302;
}

View File

@ -27,5 +27,5 @@ message Ip6over4 {
}
extend Protocol {
optional Ip6over4 ip6over4 = 133;
optional Ip6over4 ip6over4 = 303;
}

View File

@ -33,5 +33,5 @@ extend Ip6over6 {
}
extend Protocol {
optional Ip6over6 ip6over6 = 136;
optional Ip6over6 ip6over6 = 306;
}

View File

@ -28,5 +28,5 @@ message Llc {
}
extend Protocol {
optional Llc llc = 123;
optional Llc llc = 202;
}

View File

@ -44,5 +44,5 @@ message Mac {
}
extend Protocol {
optional Mac mac = 51;
optional Mac mac = 100;
}

View File

@ -22,6 +22,7 @@ FORMS += \
sample.ui
PROTOS += \
protocol.proto \
fileformat.proto \
mac.proto \
payload.proto \
eth2.proto \
@ -45,10 +46,11 @@ PROTOS += \
udp.proto \
textproto.proto \
userscript.proto \
sample.proto
sample.proto
HEADERS += \
abstractprotocol.h \
comboprotocol.h \
fileformat.h \
protocolmanager.h \
protocollist.h \
protocollistiterator.h \
@ -79,6 +81,8 @@ HEADERS += \
sample.h
SOURCES += \
abstractprotocol.cpp \
crc32c.cpp \
fileformat.cpp \
protocolmanager.cpp \
protocollist.cpp \
protocollistiterator.cpp \
@ -101,19 +105,6 @@ SOURCES += \
userscript.cpp \
sample.cpp
protobuf_decl.name = protobuf header
protobuf_decl.input = PROTOS
protobuf_decl.output = ${QMAKE_FILE_BASE}.pb.h
protobuf_decl.commands = protoc --cpp_out="." ${QMAKE_FILE_NAME}
protobuf_decl.variable_out = GENERATED_FILES
QMAKE_EXTRA_COMPILERS += protobuf_decl
protobuf_impl.name = protobuf implementation
protobuf_impl.input = PROTOS
protobuf_impl.output = ${QMAKE_FILE_BASE}.pb.cc
protobuf_impl.depends = ${QMAKE_FILE_BASE}.pb.h
protobuf_impl.commands = $$escape_expand(\n)
protobuf_impl.variable_out = GENERATED_SOURCES
QMAKE_EXTRA_COMPILERS += protobuf_impl
QMAKE_DISTCLEAN += object_script.*
include(../protobuf.pri)

View File

@ -37,5 +37,5 @@ message Payload {
}
extend Protocol {
optional Payload payload = 52;
optional Payload payload = 101;
}

View File

@ -78,41 +78,41 @@ message Protocol {
required ProtocolId protocol_id = 1;
extensions 51 to 100; // Reserved for Ostinato Use
extensions 101 to 200; // Available for use by protocols
extensions 100 to 199; // Reserved for Ostinato Use
extensions 200 to 500; // Available for use by protocols
enum k {
kMacFieldNumber = 51;
kPayloadFieldNumber = 52;
kSampleFieldNumber = 53;
kUserScriptFieldNumber = 54;
kMacFieldNumber = 100;
kPayloadFieldNumber = 101;
kSampleFieldNumber = 102;
kUserScriptFieldNumber = 103;
kEth2FieldNumber = 121;
kDot3FieldNumber = 122;
kLlcFieldNumber = 123;
kSnapFieldNumber = 124;
kEth2FieldNumber = 200;
kDot3FieldNumber = 201;
kLlcFieldNumber = 202;
kSnapFieldNumber = 203;
kSvlanFieldNumber = 125;
kVlanFieldNumber = 126;
kSvlanFieldNumber = 204;
kVlanFieldNumber = 205;
kDot2LlcFieldNumber = 127;
kDot2SnapFieldNumber = 128;
kVlanStackFieldNumber = 129;
kDot2LlcFieldNumber = 206;
kDot2SnapFieldNumber = 207;
kVlanStackFieldNumber = 208;
kArpFieldNumber = 130;
kIp4FieldNumber = 131;
kIp6FieldNumber = 132;
kIp6over4FieldNumber = 133;
kIp4over6FieldNumber = 134;
kIp4over4FieldNumber = 135;
kIp6over6FieldNumber = 136;
kArpFieldNumber = 300;
kIp4FieldNumber = 301;
kIp6FieldNumber = 302;
kIp6over4FieldNumber = 303;
kIp4over6FieldNumber = 304;
kIp4over4FieldNumber = 305;
kIp6over6FieldNumber = 306;
kTcpFieldNumber = 140;
kUdpFieldNumber = 141;
kIcmpFieldNumber = 142;
kIgmpFieldNumber = 143;
kTcpFieldNumber = 400;
kUdpFieldNumber = 401;
kIcmpFieldNumber = 402;
kIgmpFieldNumber = 403;
kTextProtocolFieldNumber = 150;
kTextProtocolFieldNumber = 500;
}
}

View File

@ -34,5 +34,5 @@ message Sample {
}
extend Protocol {
optional Sample sample = 53;
optional Sample sample = 102;
}

View File

@ -27,5 +27,5 @@ message Snap {
}
extend Protocol {
optional Snap snap = 124;
optional Snap snap = 203;
}

View File

@ -23,5 +23,5 @@ import "vlan.proto";
package OstProto;
extend Protocol {
optional Vlan svlan = 125;
optional Vlan svlan = 204;
}

View File

@ -42,6 +42,6 @@ message Tcp {
}
extend Protocol {
optional Tcp tcp = 140;
optional Tcp tcp = 400;
}

View File

@ -33,5 +33,5 @@ message TextProtocol {
}
extend Protocol {
optional TextProtocol textProtocol = 150;
optional TextProtocol textProtocol = 500;
}

View File

@ -35,5 +35,5 @@ message Udp {
}
extend Protocol {
optional Udp udp = 141;
optional Udp udp = 401;
}

View File

@ -29,5 +29,5 @@ message UserScript {
}
extend Protocol {
optional UserScript userScript = 54;
optional UserScript userScript = 103;
}

View File

@ -30,5 +30,5 @@ message Vlan {
}
extend Protocol {
optional Vlan vlan = 126;
optional Vlan vlan = 205;
}

View File

@ -27,5 +27,5 @@ message VlanStack {
}
extend Protocol {
optional VlanStack vlanStack = 129;
optional VlanStack vlanStack = 208;
}

33
protobuf.pri Normal file
View File

@ -0,0 +1,33 @@
#
# Qt qmake integration with Google Protocol Buffers compiler protoc
#
# To compile protocol buffers with qt qmake, specify PROTOS variable and
# include this file
#
# Example:
# PROTOS = a.proto b.proto
# include(protobuf.pri)
#
# By default protoc looks for .proto files (including the imported ones) in
# the current directory where protoc is run. If you need to include additional
# paths specify the PROTOPATH variable
#
PROTOPATH += .
PROTOPATHS =
for(p, PROTOPATH):PROTOPATHS += --proto_path=$${p}
protobuf_decl.name = protobuf header
protobuf_decl.input = PROTOS
protobuf_decl.output = ${QMAKE_FILE_BASE}.pb.h
protobuf_decl.commands = protoc --cpp_out="." $${PROTOPATHS} ${QMAKE_FILE_NAME}
protobuf_decl.variable_out = GENERATED_FILES
QMAKE_EXTRA_COMPILERS += protobuf_decl
protobuf_impl.name = protobuf implementation
protobuf_impl.input = PROTOS
protobuf_impl.output = ${QMAKE_FILE_BASE}.pb.cc
protobuf_impl.depends = ${QMAKE_FILE_BASE}.pb.h
protobuf_impl.commands = $$escape_expand(\n)
protobuf_impl.variable_out = GENERATED_SOURCES
QMAKE_EXTRA_COMPILERS += protobuf_impl