QA Pcbnew: add auto plugin mode to pcbnew tools
This means it can read all supported file types from the command line as filename. Stream input (for fuzzing) still requires the plugin to be specified and for the plugin to support a stream interface. This is useful for putting profilers onto importers without confusing it with GUI init, libraries, etc.
This commit is contained in:
@@ -23,7 +23,9 @@
|
||||
|
||||
#include <qa_utils/utility_registry.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <wx/cmdline.h>
|
||||
@@ -35,10 +37,8 @@
|
||||
#include <board_item.h>
|
||||
#include <common.h>
|
||||
#include <core/profile.h>
|
||||
#include <richio.h>
|
||||
|
||||
#include <pcb_io/allegro/pcb_io_allegro.h>
|
||||
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
|
||||
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr_parser.h>
|
||||
|
||||
#include <qa_utils/stdstream_line_reader.h>
|
||||
@@ -48,23 +48,12 @@ using PARSE_DURATION = std::chrono::microseconds;
|
||||
|
||||
|
||||
/**
|
||||
* In order to support fuzz testing, we need to be able to parse from stdin.
|
||||
* The PCB_IO interface is designed to parse from a file, so this class is a simple
|
||||
* wrapper that needs to be implemented to wrap a plugin's core parser function.
|
||||
* Generic board parser - this makes no assumption about what the source data might be.
|
||||
*/
|
||||
class STREAM_PARSER
|
||||
class BOARD_PARSER
|
||||
{
|
||||
public:
|
||||
virtual ~STREAM_PARSER() = default;
|
||||
|
||||
/**
|
||||
* Take some input stream and prepare it for parsing. This may involve reading
|
||||
* the whole stream into memory, or it may involve setting up some kind of streaming
|
||||
* reader.
|
||||
*
|
||||
* @throw IO_ERROR if there is a problem reading from the stream
|
||||
*/
|
||||
virtual void PrepareStream( std::istream& aStream ) = 0;
|
||||
virtual ~BOARD_PARSER() = default;
|
||||
|
||||
/**
|
||||
* Actually perform the parsing and return a BOARD_ITEM if successful, or nullptr if not.
|
||||
@@ -75,24 +64,62 @@ public:
|
||||
};
|
||||
|
||||
|
||||
class SEXPR_STREAM_PARSER : public STREAM_PARSER
|
||||
/**
|
||||
* Provide the BOARD_PARSER interface wrapping a normal PCB_IO file-based plugin lookup
|
||||
*/
|
||||
class FILE_PARSER : public BOARD_PARSER
|
||||
{
|
||||
public:
|
||||
void PrepareStream( std::istream& aStream ) override
|
||||
FILE_PARSER( PCB_IO_MGR::PCB_FILE_T aFileType, const wxString& aFileName ) :
|
||||
m_fileType( aFileType ),
|
||||
m_fileName( aFileName )
|
||||
{
|
||||
m_reader.SetStream( aStream );
|
||||
|
||||
m_parser = std::make_unique<PCB_IO_KICAD_SEXPR_PARSER>( &m_reader, nullptr, nullptr );
|
||||
}
|
||||
|
||||
std::unique_ptr<BOARD_ITEM> Parse() override
|
||||
{
|
||||
return std::unique_ptr<BOARD_ITEM>{ m_parser->Parse() };
|
||||
BOARD* board = PCB_IO_MGR::Load( m_fileType, m_fileName, nullptr, {}, nullptr, nullptr );
|
||||
return std::unique_ptr<BOARD_ITEM>( board );
|
||||
}
|
||||
|
||||
private:
|
||||
STDISTREAM_LINE_READER m_reader;
|
||||
std::unique_ptr<PCB_IO_KICAD_SEXPR_PARSER> m_parser;
|
||||
PCB_IO_MGR::PCB_FILE_T m_fileType;
|
||||
wxString m_fileName;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* In order to support fuzz testing, we need to be able to parse from stdin.
|
||||
* The PCB_IO interface is designed to parse from a file, so this class is a simple
|
||||
* wrapper that needs to be implemented to wrap a plugin's core parser function.
|
||||
*/
|
||||
class STREAM_PARSER : public BOARD_PARSER
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Take some input stream and prepare it for parsing. This may involve reading
|
||||
* the whole stream into memory, or it may involve setting up some kind of streaming
|
||||
* reader.
|
||||
*
|
||||
* @throw IO_ERROR if there is a problem setting up from the stream
|
||||
*/
|
||||
virtual void PrepareStream( std::istream& aStream ) = 0;
|
||||
};
|
||||
|
||||
|
||||
class SEXPR_STREAM_PARSER : public STREAM_PARSER
|
||||
{
|
||||
public:
|
||||
void PrepareStream( std::istream& aStream ) override { m_reader.SetStream( aStream ); }
|
||||
|
||||
std::unique_ptr<BOARD_ITEM> Parse() override
|
||||
{
|
||||
PCB_IO_KICAD_SEXPR_PARSER parser( &m_reader, nullptr, nullptr );
|
||||
return std::unique_ptr<BOARD_ITEM>{ parser.Parse() };
|
||||
}
|
||||
|
||||
private:
|
||||
STDISTREAM_LINE_READER m_reader;
|
||||
};
|
||||
|
||||
|
||||
@@ -101,7 +128,7 @@ class ALLEGRO_BRD_STREAM_PARSER : public STREAM_PARSER
|
||||
public:
|
||||
void PrepareStream( std::istream& aStream ) override
|
||||
{
|
||||
// Allegro parser expects to mmap the stream, so we need to
|
||||
// Allegro parser expects to mmap a file, so we need to
|
||||
// dump it all in memory to simulate that.
|
||||
m_buffer.assign( std::istreambuf_iterator<char>( aStream ), std::istreambuf_iterator<char>() );
|
||||
|
||||
@@ -130,87 +157,133 @@ private:
|
||||
};
|
||||
|
||||
|
||||
enum class PLUGIN_TYPE
|
||||
/**
|
||||
* Runs a BOARD_PARSER against a filename or stream and reports results.
|
||||
*/
|
||||
class PCB_PARSE_RUNNER
|
||||
{
|
||||
KICAD_SEXPR,
|
||||
ALLEGRO
|
||||
public:
|
||||
PCB_PARSE_RUNNER( PCB_IO_MGR::PCB_FILE_T aPluginType, bool aVerbose ) :
|
||||
m_pluginType( aPluginType ),
|
||||
m_verbose( aVerbose )
|
||||
{
|
||||
}
|
||||
|
||||
bool Parse( std::istream& aStream )
|
||||
{
|
||||
std::unique_ptr<STREAM_PARSER> parser;
|
||||
|
||||
switch( m_pluginType )
|
||||
{
|
||||
case PCB_IO_MGR::KICAD_SEXP: parser = std::make_unique<SEXPR_STREAM_PARSER>(); break;
|
||||
case PCB_IO_MGR::ALLEGRO: parser = std::make_unique<ALLEGRO_BRD_STREAM_PARSER>(); break;
|
||||
default:
|
||||
std::cerr << fmt::format( "Unsupported plugin type for streaming input: {}",
|
||||
static_cast<int>( m_pluginType ) )
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
wxCHECK( parser, false );
|
||||
|
||||
try
|
||||
{
|
||||
parser->PrepareStream( aStream );
|
||||
}
|
||||
catch( const IO_ERROR& e )
|
||||
{
|
||||
std::cerr << fmt::format( "Error preparing stream: {}", e.What().ToStdString() ) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return doParse( *parser );
|
||||
}
|
||||
|
||||
bool Parse( const wxString& aFilename )
|
||||
{
|
||||
FILE_PARSER parser( m_pluginType, aFilename );
|
||||
return doParse( parser );
|
||||
}
|
||||
|
||||
private:
|
||||
bool doParse( BOARD_PARSER& aParser )
|
||||
{
|
||||
std::unique_ptr<BOARD_ITEM> board;
|
||||
PARSE_DURATION duration{};
|
||||
|
||||
try
|
||||
{
|
||||
PROF_TIMER timer;
|
||||
board = aParser.Parse();
|
||||
duration = timer.SinceStart<PARSE_DURATION>();
|
||||
}
|
||||
catch( const IO_ERROR& e )
|
||||
{
|
||||
std::cerr << "Parsing failed: " << e.What() << std::endl;
|
||||
}
|
||||
|
||||
if( m_verbose )
|
||||
{
|
||||
std::cout << fmt::format( "Took: {}us", duration.count() ) << std::endl;
|
||||
|
||||
if( board )
|
||||
std::cout << fmt::format( " {} nets", board->GetBoard()->GetNetCount() ) << std::endl;
|
||||
}
|
||||
|
||||
return board != nullptr;
|
||||
}
|
||||
|
||||
PCB_IO_MGR::PCB_FILE_T m_pluginType;
|
||||
bool m_verbose;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Parse a PCB or footprint file from the given input stream
|
||||
*
|
||||
* @param aStream the input stream to read from
|
||||
* @return success, duration (in us)
|
||||
*/
|
||||
bool parse( std::istream& aStream, PLUGIN_TYPE aPluginType, bool aVerbose )
|
||||
{
|
||||
std::unique_ptr<STREAM_PARSER> parser;
|
||||
|
||||
switch( aPluginType )
|
||||
{
|
||||
case PLUGIN_TYPE::KICAD_SEXPR:
|
||||
parser = std::make_unique<SEXPR_STREAM_PARSER>();
|
||||
break;
|
||||
case PLUGIN_TYPE::ALLEGRO:
|
||||
parser = std::make_unique<ALLEGRO_BRD_STREAM_PARSER>();
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
parser->PrepareStream( aStream );
|
||||
}
|
||||
catch( const IO_ERROR& e )
|
||||
{
|
||||
if( aVerbose )
|
||||
{
|
||||
std::cerr << fmt::format( "Error preparing stream: {}", e.What().ToStdString() ) << std::endl;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<BOARD_ITEM> board = nullptr;
|
||||
|
||||
PARSE_DURATION duration{};
|
||||
|
||||
try
|
||||
{
|
||||
PROF_TIMER timer;
|
||||
board = parser->Parse();
|
||||
|
||||
duration = timer.SinceStart<PARSE_DURATION>();
|
||||
}
|
||||
catch( const IO_ERROR& e )
|
||||
{
|
||||
std::cout << "Parsing failed: " << e.What() << std::endl;
|
||||
}
|
||||
|
||||
if( aVerbose )
|
||||
{
|
||||
std::cout << fmt::format( "Took: {}us", duration.count() ) << std::endl;
|
||||
|
||||
if( board )
|
||||
{
|
||||
std::cout << fmt::format( " {} nets", board->GetBoard()->GetNetCount() ) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return board != nullptr;
|
||||
}
|
||||
|
||||
|
||||
static const wxCmdLineEntryDesc g_cmdLineDesc[] = {
|
||||
{ wxCMD_LINE_SWITCH, "h", "help", _( "displays help on the command line parameters" ).mb_str(),
|
||||
wxCMD_LINE_VAL_NONE, wxCMD_LINE_OPTION_HELP },
|
||||
{ wxCMD_LINE_SWITCH, "v", "verbose", _( "print parsing information" ).mb_str() },
|
||||
{ wxCMD_LINE_OPTION, "p", "plugin", _( "parser plugin to use (kicad, allegro)" ).mb_str(),
|
||||
wxCMD_LINE_VAL_STRING, wxCMD_LINE_OPTION_MANDATORY },
|
||||
{ wxCMD_LINE_OPTION, "l", "loop", _( "number of times to loop when parsing from stdin (for AFL)" ).mb_str(),
|
||||
wxCMD_LINE_VAL_NUMBER },
|
||||
{ wxCMD_LINE_PARAM, nullptr, nullptr, _( "input file" ).mb_str(), wxCMD_LINE_VAL_STRING,
|
||||
wxCMD_LINE_PARAM_OPTIONAL | wxCMD_LINE_PARAM_MULTIPLE },
|
||||
{ wxCMD_LINE_NONE }
|
||||
{
|
||||
wxCMD_LINE_SWITCH,
|
||||
"h",
|
||||
"help",
|
||||
_( "displays help on the command line parameters" ).mb_str(),
|
||||
wxCMD_LINE_VAL_NONE,
|
||||
wxCMD_LINE_OPTION_HELP,
|
||||
},
|
||||
{
|
||||
wxCMD_LINE_SWITCH,
|
||||
"v",
|
||||
"verbose",
|
||||
_( "print parsing information" ).mb_str(),
|
||||
},
|
||||
{
|
||||
wxCMD_LINE_OPTION,
|
||||
"p",
|
||||
"plugin",
|
||||
_( "parser plugin to use (kicad, allegro, etc.)" ).mb_str(),
|
||||
wxCMD_LINE_VAL_STRING,
|
||||
},
|
||||
{
|
||||
wxCMD_LINE_SWITCH,
|
||||
nullptr,
|
||||
"list-plugins",
|
||||
_( "list available plugins and exit" ).mb_str(),
|
||||
},
|
||||
{
|
||||
wxCMD_LINE_OPTION,
|
||||
"l",
|
||||
"loop",
|
||||
_( "number of times to loop when parsing from stdin (for AFL)" ).mb_str(),
|
||||
wxCMD_LINE_VAL_NUMBER,
|
||||
},
|
||||
{
|
||||
wxCMD_LINE_PARAM,
|
||||
nullptr,
|
||||
nullptr,
|
||||
_( "input file" ).mb_str(),
|
||||
wxCMD_LINE_VAL_STRING,
|
||||
wxCMD_LINE_PARAM_OPTIONAL | wxCMD_LINE_PARAM_MULTIPLE,
|
||||
},
|
||||
{
|
||||
wxCMD_LINE_NONE,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -220,12 +293,45 @@ enum PARSER_RET_CODES
|
||||
};
|
||||
|
||||
|
||||
static const std::map<std::string, PLUGIN_TYPE> pluginTypeMap = {
|
||||
{ "kicad", PLUGIN_TYPE::KICAD_SEXPR },
|
||||
{ "allegro", PLUGIN_TYPE::ALLEGRO },
|
||||
/**
|
||||
* Map from command line keys to plugin types
|
||||
*/
|
||||
static const std::map<std::string, PCB_IO_MGR::PCB_FILE_T> pluginTypeMap = {
|
||||
{ "kicad", PCB_IO_MGR::KICAD_SEXP },
|
||||
{ "legacy", PCB_IO_MGR::LEGACY },
|
||||
{ "allegro", PCB_IO_MGR::ALLEGRO },
|
||||
{ "altium", PCB_IO_MGR::ALTIUM_DESIGNER },
|
||||
{ "cadstar", PCB_IO_MGR::CADSTAR_PCB_ARCHIVE },
|
||||
{ "eagle", PCB_IO_MGR::EAGLE },
|
||||
{ "easyeda", PCB_IO_MGR::EASYEDA },
|
||||
{ "easyedapro", PCB_IO_MGR::EASYEDAPRO },
|
||||
{ "fabmaster", PCB_IO_MGR::FABMASTER },
|
||||
{ "geda", PCB_IO_MGR::GEDA_PCB },
|
||||
{ "pads", PCB_IO_MGR::PADS },
|
||||
{ "pcad", PCB_IO_MGR::PCAD },
|
||||
{ "solidworks", PCB_IO_MGR::SOLIDWORKS_PCB },
|
||||
// { "ipc2581", PCB_IO_MGR::IPC2581 }, // readers
|
||||
// { "odbpp", PCB_IO_MGR::ODBPP },
|
||||
};
|
||||
|
||||
|
||||
static PCB_IO_MGR::PCB_FILE_T FindPluginTypeFromParams( const wxString& aExplicitPlugin, const wxString& aPath )
|
||||
{
|
||||
if( aExplicitPlugin == "auto" )
|
||||
{
|
||||
// Try to guess the plugin type from the first file
|
||||
return PCB_IO_MGR::FindPluginTypeFromBoardPath( aPath );
|
||||
}
|
||||
|
||||
auto pluginIt = pluginTypeMap.find( aExplicitPlugin.ToStdString() );
|
||||
if( pluginIt == pluginTypeMap.end() )
|
||||
{
|
||||
return PCB_IO_MGR::FILE_TYPE_NONE;
|
||||
}
|
||||
return pluginIt->second;
|
||||
}
|
||||
|
||||
|
||||
int pcb_parser_main_func( int argc, char** argv )
|
||||
{
|
||||
#ifdef __AFL_COMPILER
|
||||
@@ -247,22 +353,51 @@ int pcb_parser_main_func( int argc, char** argv )
|
||||
}
|
||||
|
||||
const bool verbose = cl_parser.Found( "verbose" );
|
||||
|
||||
if( cl_parser.Found( "list-plugins" ) )
|
||||
{
|
||||
for( const auto& [name, type] : pluginTypeMap )
|
||||
{
|
||||
std::cout << name << std::endl;
|
||||
}
|
||||
std::cout << "auto" << std::endl;
|
||||
|
||||
return KI_TEST::RET_CODES::OK;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
const size_t file_count = cl_parser.GetParamCount();
|
||||
|
||||
wxString plugin;
|
||||
wxString plugin( "auto" );
|
||||
cl_parser.Found( "plugin", &plugin );
|
||||
|
||||
auto pluginIt = pluginTypeMap.find( plugin.ToStdString() );
|
||||
if( pluginIt == pluginTypeMap.end() )
|
||||
long aflLoopCount = 1;
|
||||
cl_parser.Found( "loop", &aflLoopCount );
|
||||
|
||||
if( file_count == 0 && plugin == "auto" )
|
||||
{
|
||||
std::cerr << fmt::format( "Unknown plugin: {}", plugin.ToStdString() ) << std::endl;
|
||||
std::cerr << "When parsing from stdin, you must specify the plugin type with -p" << std::endl;
|
||||
return KI_TEST::RET_CODES::BAD_CMDLINE;
|
||||
}
|
||||
const PLUGIN_TYPE pluginType = pluginIt->second;
|
||||
|
||||
long aflLoopCount = 1;
|
||||
cl_parser.Found( "loop", &aflLoopCount );
|
||||
const PCB_IO_MGR::PCB_FILE_T pluginType =
|
||||
FindPluginTypeFromParams( plugin, file_count > 0 ? cl_parser.GetParam( 0 ) : wxString( "" ) );
|
||||
|
||||
if( pluginType == PCB_IO_MGR::FILE_TYPE_NONE )
|
||||
{
|
||||
std::cerr << fmt::format( "Failed to determine plugin type for input using plugin {}", plugin.ToStdString() )
|
||||
<< std::endl;
|
||||
return KI_TEST::RET_CODES::BAD_CMDLINE;
|
||||
}
|
||||
|
||||
if( verbose )
|
||||
{
|
||||
std::cout << "Using plugin type: " << PCB_IO_MGR::ShowType( pluginType ) << std::endl;
|
||||
}
|
||||
|
||||
PCB_PARSE_RUNNER runner( pluginType, verbose );
|
||||
|
||||
std::vector<std::string> failedFiles;
|
||||
|
||||
if( file_count == 0 )
|
||||
{
|
||||
@@ -272,7 +407,7 @@ int pcb_parser_main_func( int argc, char** argv )
|
||||
while( __AFL_LOOP( aflLoopCount ) )
|
||||
#endif
|
||||
{
|
||||
ok = parse( std::cin, pluginType, verbose );
|
||||
ok = runner.Parse( std::cin );
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -280,20 +415,26 @@ int pcb_parser_main_func( int argc, char** argv )
|
||||
// Parse 'n' files given on the command line
|
||||
// (this is useful for input minimisation (e.g. afl-tmin) as
|
||||
// well as manual testing
|
||||
for( unsigned i = 0; i < file_count; i++ )
|
||||
for( size_t i = 0; i < file_count; i++ )
|
||||
{
|
||||
const std::string filename = cl_parser.GetParam( i ).ToStdString();
|
||||
const wxString filename = cl_parser.GetParam( i );
|
||||
|
||||
if( verbose )
|
||||
std::cout << fmt::format( "Parsing: {}", filename ) << std::endl;
|
||||
std::cout << fmt::format( "Parsing: {}", filename.ToStdString() ) << std::endl;
|
||||
|
||||
std::ifstream fin;
|
||||
fin.open( filename );
|
||||
|
||||
ok = ok && parse( fin, pluginType, verbose );
|
||||
if( !runner.Parse( filename ) )
|
||||
{
|
||||
ok = false;
|
||||
failedFiles.push_back( filename.ToStdString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for( const auto& failedFile : failedFiles )
|
||||
{
|
||||
std::cerr << fmt::format( "Failed to parse: {}", failedFile ) << std::endl;
|
||||
}
|
||||
|
||||
if( !ok )
|
||||
return PARSER_RET_CODES::PARSE_FAILED;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user