PR-757 First pass at a new mechanism for handling services
This commit is contained in:
parent
c90b92da72
commit
06e2602e01
|
@ -227,7 +227,7 @@ if(OPLC_MAIN_PROGRAM)
|
|||
message("Compile main program enabled")
|
||||
|
||||
set(OPLC_USER_DIR ${PROJECT_SOURCE_DIR}/etc/src)
|
||||
file(GLOB oplc_SRC runtime/core/*.cpp runtime/core/dnp3s/*.cpp runtime/vendor/inih-r46/*.c)
|
||||
file(GLOB oplc_SRC runtime/core/*.cpp runtime/core/dnp3s/*.cpp runtime/core/service/*.cpp runtime/vendor/inih-r46/*.c)
|
||||
|
||||
include_directories(${OPLC_USER_DIR})
|
||||
include_directories(runtime/core)
|
||||
|
|
|
@ -26,7 +26,7 @@ endif()
|
|||
include_directories(lib)
|
||||
|
||||
# The primary source is everything in this directory
|
||||
file(GLOB oplc_SRC *.cpp dnp3s/*.cpp)
|
||||
file(GLOB oplc_SRC *.cpp dnp3s/*.cpp service/*.cpp)
|
||||
|
||||
message("In runtime")
|
||||
message(${oplc_SRC})
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
#include "dnp3.h"
|
||||
#include "dnp3_publisher.h"
|
||||
#include "dnp3_receiver.h"
|
||||
#include "../service/service_definition.h"
|
||||
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
|
@ -363,10 +364,10 @@ OutstationStackConfig dnp3_create_config(istream& cfg_stream,
|
|||
return config;
|
||||
}
|
||||
|
||||
void dnp3StartServer(int port,
|
||||
unique_ptr<istream, function<void(istream*)>>& cfg_stream,
|
||||
bool* run,
|
||||
const GlueVariablesBinding& glue_variables) {
|
||||
void dnp3s_start_server(int port,
|
||||
unique_ptr<istream, function<void(istream*)>>& cfg_stream,
|
||||
volatile bool& run,
|
||||
const GlueVariablesBinding& glue_variables) {
|
||||
const uint32_t FILTERS = levels::NORMAL;
|
||||
|
||||
Dnp3IndexedGroup binary_commands = {0};
|
||||
|
@ -409,7 +410,7 @@ void dnp3StartServer(int port,
|
|||
clock_gettime(CLOCK_MONOTONIC, &timer_start);
|
||||
|
||||
// Run this until we get a signal to stop.
|
||||
while (*run) {
|
||||
while (run) {
|
||||
{
|
||||
// Create a scope so we release the log after the read/write
|
||||
lock_guard<mutex> guard(*glue_variables.buffer_lock);
|
||||
|
@ -435,15 +436,15 @@ void dnp3StartServer(int port,
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Function to begin DNP3 server functions. This is the normal way that
|
||||
/// the DNP3 server is started.
|
||||
/// @param port The port to run against.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void dnp3s_start_server(int port, bool* run, const GlueVariablesBinding& binding) {
|
||||
void dnp3s_service_run(const GlueVariablesBinding& binding, volatile bool& run, const char* config) {
|
||||
unique_ptr<istream, function<void(istream*)>> cfg_stream(new ifstream("./../webserver/dnp3.cfg"), [](istream* s)
|
||||
{
|
||||
reinterpret_cast<ifstream*>(s)->close();
|
||||
delete s;
|
||||
});
|
||||
dnp3StartServer(port, cfg_stream, run, binding);
|
||||
int port = strlen(config) > 0 ? atoi(config) : 20000;
|
||||
dnp3s_start_server(port, cfg_stream, run, binding);
|
||||
}
|
||||
|
||||
#endif // OPLC_DNP3_OUTSTATION
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
#include <cstdint>
|
||||
#include <istream>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
@ -25,6 +25,9 @@
|
|||
namespace asiodnp3 {
|
||||
class OutstationStackConfig;
|
||||
}
|
||||
namespace std {
|
||||
class mutex;
|
||||
}
|
||||
|
||||
struct GlueVariable;
|
||||
struct GlueVariablesBinding;
|
||||
|
@ -152,11 +155,25 @@ asiodnp3::OutstationStackConfig dnp3_create_config(std::istream& cfg_stream,
|
|||
/// this signal is false.
|
||||
/// @param glue_variables The glue variables that may be bound into this
|
||||
/// server.
|
||||
void dnp3StartServer(int port,
|
||||
std::unique_ptr<std::istream, std::function<void(std::istream*)>>& cfg_stream,
|
||||
bool* run,
|
||||
const GlueVariablesBinding& glue_variables);
|
||||
void dnp3s_start_server(int port,
|
||||
std::unique_ptr<std::istream, std::function<void(std::istream*)>>& cfg_stream,
|
||||
volatile bool& run,
|
||||
const GlueVariablesBinding& glue_variables);
|
||||
|
||||
#endif // CORE_DNP3S_DNP3_H_
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Start the DNP3 server running on the specified port and configured
|
||||
/// using the specified stream.
|
||||
///
|
||||
/// The stream is specified as a function so that this function will close the
|
||||
/// stream as soon as it is done with the stream.
|
||||
/// @param glue_variables The glue variables that may be bound into this
|
||||
/// server.
|
||||
/// @param run A signal for running this server. This server terminates when
|
||||
/// this signal is false.
|
||||
/// @param config The custom configuration for this service.
|
||||
void dnp3s_service_run(const GlueVariablesBinding& binding,
|
||||
volatile bool& run, const char* config);
|
||||
|
||||
/** @}*/
|
||||
|
||||
#endif // CORE_DNP3S_DNP3_H_
|
||||
|
|
|
@ -36,9 +36,111 @@
|
|||
|
||||
#define ENIP_MIN_LENGTH 28
|
||||
|
||||
using namespace std;
|
||||
|
||||
thread_local unsigned char enip_session[4]; // opens thread for session
|
||||
|
||||
using namespace std;
|
||||
struct enip_header
|
||||
{
|
||||
unsigned char *command;//[2];
|
||||
unsigned char *length;//[2];
|
||||
unsigned char *session_handle;//[4];
|
||||
unsigned char *status;//[4];
|
||||
unsigned char *sender_context;//[8];
|
||||
unsigned char *options;//[4];
|
||||
unsigned char *data;
|
||||
};
|
||||
|
||||
struct enip_data_Unknown
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2]
|
||||
unsigned char *item1_length;//[2]
|
||||
unsigned char *item1_data;//[1]
|
||||
|
||||
unsigned char *item2_id;//[2]
|
||||
unsigned char *item2_length;//[2]
|
||||
unsigned char *item2_data;
|
||||
};
|
||||
|
||||
|
||||
struct enip_data_Unconnected
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2]
|
||||
unsigned char *item1_length;//[2]
|
||||
|
||||
unsigned char *item2_id;//[2]
|
||||
unsigned char *item2_length;//[2]
|
||||
|
||||
unsigned char *service;//[1] 0x4b (Request)
|
||||
unsigned char *request_pathSize;//[1]
|
||||
unsigned char *request_path;//[4]
|
||||
unsigned char *requestor_idLength;//[1]
|
||||
unsigned char *vendor_id;//[2]
|
||||
unsigned char *serial_number;//[4]
|
||||
unsigned char *data;
|
||||
};
|
||||
|
||||
|
||||
struct enip_data_Connected
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2]
|
||||
unsigned char *item1_length;//[2]
|
||||
|
||||
unsigned char *item2_id;//[2]
|
||||
unsigned char *item2_length;//[2]
|
||||
|
||||
unsigned char *service;//[1] 0x4b (Request)
|
||||
unsigned char *request_pathSize;//[1] -----------size in words
|
||||
unsigned char *request_path;//[4]
|
||||
unsigned char *actual_timeout;//[2]
|
||||
unsigned char *o2t_netConnectID;//[4]
|
||||
unsigned char *t2o_netConnectID;//[4]
|
||||
unsigned char *connect_serialNo;//[2]
|
||||
unsigned char *orig_vendorNo;//[2]
|
||||
unsigned char *orig_serialNo;//[4]
|
||||
unsigned char *timeout_multiplier;//[1]
|
||||
unsigned char *reserved;//[3]
|
||||
unsigned char *o2t_rpi;//[4]
|
||||
unsigned char *o2t_netConnectParam;//[2]
|
||||
unsigned char *t2o_rpi;//[4]
|
||||
unsigned char *t2o_netConnectParam;//[2]
|
||||
unsigned char *transport_trigger;//[1]
|
||||
unsigned char *connection_pathSize;//[1] ----- size in words
|
||||
unsigned char *connection_path;//[?]
|
||||
};
|
||||
|
||||
struct enip_data_Connected_0x70
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2] --- 0x a1 00
|
||||
unsigned char *item1_length;//[2] --- 0x 04 00
|
||||
unsigned char *connection_id;//[4]
|
||||
|
||||
unsigned char *item2_id;//[2] --- 0x b1 00
|
||||
unsigned char *item2_length;//[2] length in bytes after this byte
|
||||
unsigned char *sequence_count;//[2]
|
||||
|
||||
unsigned char *service;//[1] --- 0x4b
|
||||
unsigned char *request_pathSize;//[1] --- 0x 20 67 24 01
|
||||
unsigned char *request_path;//[4]
|
||||
unsigned char *requestor_id;//[7]
|
||||
unsigned char *pcccData;//[?]
|
||||
};
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
|
|
@ -23,106 +23,4 @@
|
|||
* @{
|
||||
*/
|
||||
|
||||
struct enip_header
|
||||
{
|
||||
unsigned char *command;//[2];
|
||||
unsigned char *length;//[2];
|
||||
unsigned char *session_handle;//[4];
|
||||
unsigned char *status;//[4];
|
||||
unsigned char *sender_context;//[8];
|
||||
unsigned char *options;//[4];
|
||||
unsigned char *data;
|
||||
};
|
||||
|
||||
struct enip_data_Unknown
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2]
|
||||
unsigned char *item1_length;//[2]
|
||||
unsigned char *item1_data;//[1]
|
||||
|
||||
unsigned char *item2_id;//[2]
|
||||
unsigned char *item2_length;//[2]
|
||||
unsigned char *item2_data;
|
||||
};
|
||||
|
||||
|
||||
struct enip_data_Unconnected
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2]
|
||||
unsigned char *item1_length;//[2]
|
||||
|
||||
unsigned char *item2_id;//[2]
|
||||
unsigned char *item2_length;//[2]
|
||||
|
||||
unsigned char *service;//[1] 0x4b (Request)
|
||||
unsigned char *request_pathSize;//[1]
|
||||
unsigned char *request_path;//[4]
|
||||
unsigned char *requestor_idLength;//[1]
|
||||
unsigned char *vendor_id;//[2]
|
||||
unsigned char *serial_number;//[4]
|
||||
unsigned char *data;
|
||||
};
|
||||
|
||||
|
||||
struct enip_data_Connected
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2]
|
||||
unsigned char *item1_length;//[2]
|
||||
|
||||
unsigned char *item2_id;//[2]
|
||||
unsigned char *item2_length;//[2]
|
||||
|
||||
unsigned char *service;//[1] 0x4b (Request)
|
||||
unsigned char *request_pathSize;//[1] -----------size in words
|
||||
unsigned char *request_path;//[4]
|
||||
unsigned char *actual_timeout;//[2]
|
||||
unsigned char *o2t_netConnectID;//[4]
|
||||
unsigned char *t2o_netConnectID;//[4]
|
||||
unsigned char *connect_serialNo;//[2]
|
||||
unsigned char *orig_vendorNo;//[2]
|
||||
unsigned char *orig_serialNo;//[4]
|
||||
unsigned char *timeout_multiplier;//[1]
|
||||
unsigned char *reserved;//[3]
|
||||
unsigned char *o2t_rpi;//[4]
|
||||
unsigned char *o2t_netConnectParam;//[2]
|
||||
unsigned char *t2o_rpi;//[4]
|
||||
unsigned char *t2o_netConnectParam;//[2]
|
||||
unsigned char *transport_trigger;//[1]
|
||||
unsigned char *connection_pathSize;//[1] ----- size in words
|
||||
unsigned char *connection_path;//[?]
|
||||
};
|
||||
|
||||
struct enip_data_Connected_0x70
|
||||
{
|
||||
unsigned char *interface_handle;//[4]
|
||||
unsigned char *timeout;//[2]
|
||||
unsigned char *item_count;//[2]
|
||||
|
||||
unsigned char *item1_id;//[2] --- 0x a1 00
|
||||
unsigned char *item1_length;//[2] --- 0x 04 00
|
||||
unsigned char *connection_id;//[4]
|
||||
|
||||
unsigned char *item2_id;//[2] --- 0x b1 00
|
||||
unsigned char *item2_length;//[2] length in bytes after this byte
|
||||
unsigned char *sequence_count;//[2]
|
||||
|
||||
unsigned char *service;//[1] --- 0x4b
|
||||
unsigned char *request_pathSize;//[1] --- 0x 20 67 24 01
|
||||
unsigned char *request_path;//[4]
|
||||
unsigned char *requestor_id;//[7]
|
||||
unsigned char *pcccData;//[?]
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
|
|
@ -108,15 +108,16 @@ struct GlueBoolGroup {
|
|||
#ifndef OPLC_GLUE_VARIABLE
|
||||
#define OPLC_GLUE_VARIABLE
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Defines the mapping for a glued variable. This defines a simple, space
|
||||
/// efficient lookup table. It has all of the mapping information that you
|
||||
/// need to find the variable based on the location name (e.g. %IB1.1). While
|
||||
/// this is space efficient, this should be searched once to construct a fast
|
||||
/// lookup into this table used for the remainder of the application lifecycle.
|
||||
|
||||
/// @brief Defines the mapping for a glued variable. This defines a simple,
|
||||
/// space efficient lookup table. It has all of the mapping information that
|
||||
/// you need to find the variable based on the location name (e.g. %IB1.1).
|
||||
/// While this is space efficient, this should be searched once to construct a
|
||||
/// fast lookup into this table used for the remainder of the application
|
||||
/// lifecycle.
|
||||
///
|
||||
/// This definition must be consistent with what is produced by the @ref glue_generator.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// This definition must be consistent with what is produced by the
|
||||
/// @ref glue_generator.
|
||||
struct GlueVariable {
|
||||
/// The direction of the variable - this is determined by I/Q/M.
|
||||
IecLocationDirection dir;
|
||||
|
@ -137,15 +138,14 @@ struct GlueVariable {
|
|||
};
|
||||
#endif // OPLC_GLUE_VARIABLE
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Defines accessors for glue variables.
|
||||
/// This structure wraps up items that are available as globals, but this allows
|
||||
/// a straighforward way to inject definitions into tests, so it is preferred
|
||||
/// to use this structure rather than globals.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// This structure wraps up items that are available as globals, but this
|
||||
/// allows a straighforward way to inject definitions into tests, so it is
|
||||
/// preferred to use this structure rather than globals.
|
||||
struct GlueVariablesBinding {
|
||||
|
||||
GlueVariablesBinding(std::mutex* buffer_lock, const std::uint16_t size, const GlueVariable* glue_variables) :
|
||||
GlueVariablesBinding(std::mutex* buffer_lock, const std::uint16_t size,
|
||||
const GlueVariable* glue_variables) :
|
||||
buffer_lock(buffer_lock),
|
||||
size(size),
|
||||
glue_variables(glue_variables)
|
||||
|
@ -161,23 +161,20 @@ struct GlueVariablesBinding {
|
|||
/// @brief The glue variables array
|
||||
const GlueVariable* glue_variables;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Find a glue varia&glue_mutexble based on the specification of the variable.
|
||||
/// @brief Find a glue varia&glue_mutexble based on the specification of
|
||||
/// the variable.
|
||||
/// @return the variable or null if there is no variable that matches all
|
||||
/// criteria in the specification.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
const GlueVariable* find(IecLocationDirection dir,
|
||||
IecLocationSize size,
|
||||
std::uint16_t msi,
|
||||
std::uint8_t lsi) const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Find a glue variable based on the location of the variable, for example
|
||||
/// %IX0.1
|
||||
/// @brief Find a glue variable based on the location of the variable, for
|
||||
/// example %IX0.1
|
||||
/// @return the variable or null if there is no variable that matches all
|
||||
/// criteria in the specification.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
const GlueVariable* find(const std::string& location) const;
|
||||
const GlueVariable* find(const std::string& location) const;
|
||||
};
|
||||
|
||||
#endif // CORE_GLUE_H
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <netdb.h>
|
||||
#include <string.h>
|
||||
|
@ -38,6 +39,8 @@
|
|||
#include "glue.h"
|
||||
#include "ladder.h"
|
||||
#include "logsink.h"
|
||||
#include "service/service_definition.h"
|
||||
#include "service/service_registry.h"
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
|
@ -46,11 +49,8 @@
|
|||
//Global Variables
|
||||
bool run_modbus = 0;
|
||||
uint16_t modbus_port = 502;
|
||||
bool run_dnp3 = 0;
|
||||
uint16_t dnp3_port = 20000;
|
||||
bool run_enip = 0;
|
||||
uint16_t enip_port = 44818;
|
||||
bool run_pstorage = 0;
|
||||
uint16_t pstorage_polling = 10;
|
||||
unsigned char server_command[1024];
|
||||
int command_index = 0;
|
||||
|
@ -60,7 +60,6 @@ time_t end_time;
|
|||
|
||||
//Global Threads
|
||||
pthread_t modbus_thread;
|
||||
pthread_t dnp3_thread;
|
||||
pthread_t enip_thread;
|
||||
pthread_t pstorage_thread;
|
||||
|
||||
|
@ -69,6 +68,8 @@ pthread_t pstorage_thread;
|
|||
unsigned char log_buffer[LOG_BUFFER_SIZE];
|
||||
std::shared_ptr<buffered_sink> log_sink;
|
||||
|
||||
using namespace std;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Start the Modbus Thread
|
||||
/// @param *arg
|
||||
|
@ -79,19 +80,6 @@ void *modbusThread(void *arg)
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Start the DNP3 Thread
|
||||
/// @param *arg
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void *dnp3Thread(void *arg)
|
||||
{
|
||||
GlueVariablesBinding binding(&bufferLock, OPLCGLUE_GLUE_SIZE, oplc_glue_vars);
|
||||
dnp3StartServer(dnp3_port, &run_dnp3, binding);
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Start the Enip Thread
|
||||
/// @param *arg
|
||||
|
@ -102,16 +90,6 @@ void *enipThread(void *arg)
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Start the Persistent Storage Thread
|
||||
/// @param *arg
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void *pstorageThread(void *arg)
|
||||
{
|
||||
startPstorage();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Read the argument from a command function
|
||||
/// @param *command
|
||||
|
@ -135,6 +113,42 @@ int readCommandArgument(unsigned char *command)
|
|||
return atoi((char *)argument);
|
||||
}
|
||||
|
||||
/// Copy the configuration argument from the command into the buffer
|
||||
/// @param source The source of the command. This should be pointing to the
|
||||
/// character right after the opening "("
|
||||
/// @param target The target buffer for the command.
|
||||
/// @return Zero on success. Non-zero on failure. If this function
|
||||
/// fails, it marks the target as an empty string so it is still safe
|
||||
/// to read the string but it will be empty.
|
||||
std::int8_t copy_command_config(unsigned char *source, char target[],
|
||||
size_t target_size)
|
||||
{
|
||||
// Search through source until we find the closing ")"
|
||||
size_t end_index = 0;
|
||||
while (source[end_index] != ')' && source[end_index] != '\0') {
|
||||
end_index++;
|
||||
}
|
||||
|
||||
// If the size is such that we would not be able to assign the
|
||||
// terminating null, then return an error.
|
||||
if (end_index >= (target_size - 1)) {
|
||||
target[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Now we want to copy everything from the beginning of source
|
||||
// into target, up to the length of target. This may or many not
|
||||
// fill our target, but the size ensure we don't go over the buffer
|
||||
// size.
|
||||
std::memcpy(target, source, min(end_index, target_size));
|
||||
|
||||
// And set the final null terminating character in target. In general
|
||||
// this is replacing the null terminating character with the right end.
|
||||
target[end_index] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Create the socket and bind it.
|
||||
/// @param port
|
||||
|
@ -233,6 +247,10 @@ void processCommand(unsigned char *buffer, int client_fd)
|
|||
spdlog::debug("Process command received {}", buffer);
|
||||
|
||||
int count_char = 0;
|
||||
|
||||
// A buffer for the command configuration information.
|
||||
const size_t COMMAND_CONFIG_SIZE(1024);
|
||||
char command_config[COMMAND_CONFIG_SIZE];
|
||||
|
||||
if (processing_command)
|
||||
{
|
||||
|
@ -251,12 +269,7 @@ void processCommand(unsigned char *buffer, int client_fd)
|
|||
pthread_join(modbus_thread, NULL);
|
||||
spdlog::info("Modbus server was stopped");
|
||||
}
|
||||
if (run_dnp3)
|
||||
{
|
||||
run_dnp3 = 0;
|
||||
pthread_join(dnp3_thread, NULL);
|
||||
spdlog::info("DNP3 server was stopped");
|
||||
}
|
||||
stop_services();
|
||||
run_openplc = 0;
|
||||
processing_command = false;
|
||||
}
|
||||
|
@ -294,32 +307,19 @@ void processCommand(unsigned char *buffer, int client_fd)
|
|||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
else if (strncmp(buffer, "start_dnp3(", 11) == 0)
|
||||
{
|
||||
|
||||
processing_command = true;
|
||||
dnp3_port = readCommandArgument(buffer);
|
||||
spdlog::info("Issued start_dnp3() command to start on port: {}", dnp3_port);
|
||||
if (run_dnp3)
|
||||
{
|
||||
spdlog::info("DNP3 server already active. Restarting on port: {}", dnp3_port);
|
||||
//Stop DNP3 server
|
||||
run_dnp3 = 0;
|
||||
pthread_join(dnp3_thread, NULL);
|
||||
spdlog::info("DNP3 server was stopped");
|
||||
ServiceDefinition* def = find_service("dnp3s");
|
||||
if (def && copy_command_config(buffer + 11, command_config, COMMAND_CONFIG_SIZE) == 0) {
|
||||
def->start(command_config);
|
||||
}
|
||||
//Start DNP3 server
|
||||
run_dnp3 = 1;
|
||||
pthread_create(&dnp3_thread, NULL, dnp3Thread, NULL);
|
||||
processing_command = false;
|
||||
}
|
||||
else if (strncmp(buffer, "stop_dnp3()", 11) == 0)
|
||||
{
|
||||
processing_command = true;
|
||||
spdlog::info("Issued stop_dnp3() command");
|
||||
if (run_dnp3)
|
||||
{
|
||||
run_dnp3 = 0;
|
||||
pthread_join(dnp3_thread, NULL);
|
||||
spdlog::info("DNP3 server was stopped");
|
||||
ServiceDefinition* def = find_service("dnp3s");
|
||||
if (def) {
|
||||
def->stop();
|
||||
}
|
||||
processing_command = false;
|
||||
}
|
||||
|
@ -387,25 +387,18 @@ void processCommand(unsigned char *buffer, int client_fd)
|
|||
else if (strncmp(buffer, "start_pstorage(", 15) == 0)
|
||||
{
|
||||
processing_command = true;
|
||||
pstorage_polling = readCommandArgument(buffer);
|
||||
spdlog::info("Issued start_pstorage() command with polling rate of {} seconds", pstorage_polling);
|
||||
if (run_pstorage)
|
||||
{
|
||||
spdlog::info("Persistent Storage server already active. Changing polling rate to: {}", pstorage_polling);
|
||||
ServiceDefinition* def = find_service("pstorage");
|
||||
if (def && copy_command_config(buffer + 15, command_config, COMMAND_CONFIG_SIZE) == 0) {
|
||||
def->start(command_config);
|
||||
}
|
||||
//Start Enip server
|
||||
run_pstorage = 1;
|
||||
pthread_create(&pstorage_thread, NULL, pstorageThread, NULL);
|
||||
processing_command = false;
|
||||
}
|
||||
else if (strncmp(buffer, "stop_pstorage()", 15) == 0)
|
||||
{
|
||||
processing_command = true;
|
||||
spdlog::info("Issued stop_pstorage() command");
|
||||
if (run_pstorage)
|
||||
{
|
||||
run_pstorage = 0;
|
||||
spdlog::info("Persistent Storage thread was stopped");
|
||||
ServiceDefinition* def = find_service("pstorage");
|
||||
if (def) {
|
||||
def->stop();
|
||||
}
|
||||
processing_command = false;
|
||||
}
|
||||
|
@ -538,11 +531,8 @@ void startInteractiveServer(int port)
|
|||
}
|
||||
spdlog::info("Shutting down internal threads\n");
|
||||
run_modbus = 0;
|
||||
run_dnp3 = 0;
|
||||
run_enip = 0;
|
||||
run_pstorage = 0;
|
||||
pthread_join(modbus_thread, NULL);
|
||||
pthread_join(dnp3_thread, NULL);
|
||||
pthread_join(enip_thread, NULL);
|
||||
|
||||
spdlog::info("Closing socket...");
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
#include <pthread.h>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
//#include "enipStruct.h" //This header file contains necessary structs for enip.cpp
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
|
@ -89,6 +88,7 @@ struct GlueVariablesBinding;
|
|||
|
||||
extern const std::uint16_t OPLCGLUE_GLUE_SIZE;
|
||||
extern const GlueVariable oplc_glue_vars[];
|
||||
extern const std::size_t OPLC_LOCATED_VARIABLES_MEM_SIZE;
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
//FUNCTION PROTOTYPES
|
||||
|
@ -135,10 +135,7 @@ bool SetSocketBlockingEnabled(int fd, bool blocking);
|
|||
void startInteractiveServer(int port);
|
||||
void initializeLogging(int argc,char **argv);
|
||||
extern bool run_modbus;
|
||||
extern bool run_dnp3;
|
||||
extern bool run_enip;
|
||||
extern bool run_pstorage;
|
||||
extern uint16_t pstorage_polling;
|
||||
extern time_t start_time;
|
||||
extern time_t end_time;
|
||||
|
||||
|
@ -163,8 +160,4 @@ void updateBuffersOut_MB();
|
|||
void dnp3StartServer(int port, bool* run, const GlueVariablesBinding& binding);
|
||||
#endif
|
||||
|
||||
//persistent_storage.cpp
|
||||
void startPstorage();
|
||||
int readPersistentStorage();
|
||||
|
||||
/** @}*/
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
|
||||
#include "iec_types.h"
|
||||
#include "ladder.h"
|
||||
#include "service/service_definition.h"
|
||||
#include "service/service_registry.h"
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
|
@ -191,7 +193,10 @@ int main(int argc,char **argv)
|
|||
//======================================================
|
||||
glueVars();
|
||||
mapUnusedIO();
|
||||
readPersistentStorage();
|
||||
ServiceDefinition* pstorageDef = find_service("pstorage");
|
||||
if (pstorageDef) {
|
||||
pstorageDef->initialize();
|
||||
}
|
||||
//pthread_t persistentThread;
|
||||
//pthread_create(&persistentThread, NULL, persistentStorage, NULL);
|
||||
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright 2019 Thiago Alves
|
||||
// This file is part of the OpenPLC Software Stack.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
//
|
||||
// This file is responsible for the persistent storage on the OpenPLC
|
||||
// Thiago Alves, Jun 2019
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
#include <mutex>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "ladder.h"
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Main function for the thread. Should create a buffer for the persistent
|
||||
/// data, compare it with the actual data and write back to the persistent
|
||||
/// file if the data has changed
|
||||
////////////////////////////////////////////////////////////////////////////////-
|
||||
void startPstorage()
|
||||
{
|
||||
unsigned char log_msg[1000];
|
||||
IEC_UINT persistentBuffer[BUFFER_SIZE];
|
||||
|
||||
//Read initial buffers into persistent struct
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bufferLock);
|
||||
for (int i = 0; i < BUFFER_SIZE; i++)
|
||||
{
|
||||
if (int_memory[i] != NULL) persistentBuffer[i] = *int_memory[i];
|
||||
}
|
||||
}
|
||||
|
||||
//Perform the first write
|
||||
if (access("persistent.file", F_OK) == -1)
|
||||
{
|
||||
spdlog::info("Creating Persistent Storage file");
|
||||
}
|
||||
|
||||
FILE *ps = fopen("persistent.file", "w"); //if file already exists, it will be overwritten
|
||||
if (ps == NULL)
|
||||
{
|
||||
spdlog::error("Persistent Storage: Error creating persistent memory file!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fwrite(persistentBuffer, sizeof(IEC_INT), BUFFER_SIZE, ps) < BUFFER_SIZE)
|
||||
{
|
||||
spdlog::error("Persistent Storage: Error writing to persistent memory file!");
|
||||
return;
|
||||
}
|
||||
fclose(ps);
|
||||
|
||||
//Run the main thread
|
||||
while (run_pstorage)
|
||||
{
|
||||
|
||||
//Verify if persistent buffer is outdated
|
||||
bool bufferOutdated = false;
|
||||
{
|
||||
for (int i = 0; i < BUFFER_SIZE; i++)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bufferLock);
|
||||
if (int_memory[i] != NULL)
|
||||
{
|
||||
if (persistentBuffer[i] != *int_memory[i])
|
||||
{
|
||||
persistentBuffer[i] = *int_memory[i];
|
||||
bufferOutdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If buffer is outdated, write the changes back to the file
|
||||
if (bufferOutdated)
|
||||
{
|
||||
FILE *fd = fopen("persistent.file", "w"); //if file already exists, it will be overwritten
|
||||
if (fd == NULL)
|
||||
{
|
||||
spdlog::error("Persistent Storage: Error creating persistent memory file!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fwrite(persistentBuffer, sizeof(IEC_INT), BUFFER_SIZE, fd) < BUFFER_SIZE)
|
||||
{
|
||||
spdlog::error("Persistent Storage: Error writing to persistent memory file!");
|
||||
return;
|
||||
}
|
||||
fclose(fd);
|
||||
}
|
||||
|
||||
sleepms(pstorage_polling*1000);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief This function reads the contents from persistent.file into OpenPLC
|
||||
/// internal buffers.
|
||||
///
|
||||
/// Must be called when OpenPLC is initializing. If persistent storage is
|
||||
/// disabled, the persistent.file will not be found and the function will
|
||||
/// exit gracefully.
|
||||
////////////////////////////////////////////////////////////////////////////////-
|
||||
int readPersistentStorage()
|
||||
{
|
||||
unsigned char log_msg[1000];
|
||||
FILE *fd = fopen("persistent.file", "r");
|
||||
if (fd == NULL)
|
||||
{
|
||||
spdlog::warn("Warning: Persistent Storage file not found");
|
||||
return 0;
|
||||
}
|
||||
|
||||
IEC_INT persistentBuffer[BUFFER_SIZE];
|
||||
|
||||
if (fread(persistentBuffer, sizeof(IEC_INT), BUFFER_SIZE, fd) < BUFFER_SIZE)
|
||||
{
|
||||
spdlog::error("Persistent Storage: Error while trying to read persistent.file!");
|
||||
return 0;
|
||||
}
|
||||
fclose(fd);
|
||||
|
||||
spdlog::info("Persistent Storage: Reading persistent.file into local buffers");
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bufferLock);
|
||||
for (int i = 0; i < BUFFER_SIZE; i++)
|
||||
{
|
||||
if (int_memory[i] != NULL) *int_memory[i] = persistentBuffer[i];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @}*/
|
|
@ -0,0 +1,291 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright 2019 Thiago Alves
|
||||
// Copyright 2019 Smarter Grid Solutions
|
||||
// This file is part of the OpenPLC Software Stack.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
//
|
||||
// This file is responsible for the persistent storage on the OpenPLC
|
||||
// Thiago Alves, Jun 2019
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <istream>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "ladder.h"
|
||||
#include "glue.h"
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
|
||||
using namespace std;
|
||||
|
||||
#define IS_BIG_ENDIAN (*(uint16_t *)"\0\xff" < 0x100)
|
||||
|
||||
/// A file header that must be present for the persistence file. This header
|
||||
// defines a few pieces of information as indicated below besides the obvious
|
||||
// "OPLCPS" (OpenPLC Persistent Storage) header.
|
||||
const char FILE_HEADER[] = { (char)137, 'O', 'P', 'L', 'C', 'P', 'S', '\n', 'v', 0, '\n' };
|
||||
// | |
|
||||
// non-printing char version # of file format
|
||||
// to avoid accidentally matching allows 255 versions
|
||||
// a text file
|
||||
|
||||
#define OPLC_PERSISTENT_STORAGE_MAX_SIZE 1024
|
||||
|
||||
// The size of the file header
|
||||
const size_t FILE_HEADER_SIZE(extent<decltype(FILE_HEADER)>::value);
|
||||
|
||||
/// Get the size of the particular location in bytes.
|
||||
/// @param size the size type.
|
||||
/// @return The required storage size in number of bytes.
|
||||
inline uint8_t get_size_bytes(IecLocationSize size) {
|
||||
switch (size) {
|
||||
case IECLST_BIT:
|
||||
return 1;
|
||||
case IECLST_BYTE:
|
||||
return 1;
|
||||
case IECLST_WORD:
|
||||
return 2;
|
||||
case IECLST_DOUBLEWORD:
|
||||
return 4;
|
||||
case IECLST_LONGWORD:
|
||||
return 8;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Get the total number of bytes required to store the bindings.
|
||||
/// @param bindings The bindings that we want to store.
|
||||
/// @return The total number of bytes required.
|
||||
size_t get_size_bytes(const GlueVariablesBinding& bindings) {
|
||||
size_t size(0);
|
||||
|
||||
for (uint16_t index(0); index < bindings.size; ++index) {
|
||||
const GlueVariable& glue = bindings.glue_variables[index];
|
||||
|
||||
if (glue.dir != IECLDT_MEM) {
|
||||
// We only care about items that are stored in memory
|
||||
continue;
|
||||
}
|
||||
|
||||
size += get_size_bytes(glue.size);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/// Copy the glue values into the buffer.
|
||||
/// @param bindings The bindings that we want to copy from.
|
||||
/// @param buffer The buffer that we are copying into.
|
||||
/// @return The number of bytes that were written into the buffer.
|
||||
size_t pstorage_copy_glue(const GlueVariablesBinding& bindings, char* buffer) {
|
||||
lock_guard<mutex> guard(*bindings.buffer_lock);
|
||||
|
||||
size_t num_written(0);
|
||||
for (uint16_t index(0); index < bindings.size; ++index) {
|
||||
const GlueVariable& glue = bindings.glue_variables[index];
|
||||
|
||||
if (glue.dir != IECLDT_MEM) {
|
||||
// We only care about items that are stored in memory
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t num_bytes = get_size_bytes(glue.size);
|
||||
|
||||
// Write the number of bytes to the buffer
|
||||
memcpy(buffer, glue.value, num_bytes);
|
||||
|
||||
// Advance the pointer to the next starting position
|
||||
num_written += num_bytes;
|
||||
buffer += num_bytes;
|
||||
}
|
||||
|
||||
return num_written;
|
||||
}
|
||||
|
||||
int8_t pstorage_run(const GlueVariablesBinding& bindings, volatile bool& run,
|
||||
chrono::milliseconds sleep_time,
|
||||
function<std::ostream*(void)> stream_fn)
|
||||
{
|
||||
const char endianness_header[2] = { IS_BIG_ENDIAN, '\n'};
|
||||
|
||||
// This isn't ideal because we really only need enough space for
|
||||
// the located variables that are memory items, and this is all, but
|
||||
// it does ensure we have enough space.
|
||||
char buffer[OPLC_PERSISTENT_STORAGE_MAX_SIZE] = {0};
|
||||
// We keep a second block of memory so that we can detect if the values
|
||||
// have changed and not write if they are the same.
|
||||
char buffer_old[OPLC_PERSISTENT_STORAGE_MAX_SIZE] = {0};
|
||||
|
||||
// If the required size from bindings is greater than the configured
|
||||
// size, then just exit
|
||||
if (get_size_bytes(bindings) > extent<decltype(buffer)>::value) {
|
||||
spdlog::error("Stored variables too large for persistent storage");
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (run) {
|
||||
size_t num_written = pstorage_copy_glue(bindings, buffer);
|
||||
|
||||
if (memcmp(buffer, buffer_old, num_written) != 0) {
|
||||
// Try to open the file to do the initial write
|
||||
unique_ptr<ostream> out_stream(stream_fn());
|
||||
if (!out_stream) {
|
||||
spdlog::error("Unable to open persistent storage file for writing");
|
||||
return -2;
|
||||
}
|
||||
out_stream->write(FILE_HEADER, FILE_HEADER_SIZE);
|
||||
out_stream->write(endianness_header, 2);
|
||||
out_stream->write(buffer, num_written);
|
||||
|
||||
spdlog::info("Persistent storage updated");
|
||||
|
||||
// We should be able to avoid this memory copy entirely
|
||||
memcpy(buffer_old, buffer, num_written);
|
||||
} else {
|
||||
spdlog::debug("Skip persistent write because unchanged values");
|
||||
}
|
||||
|
||||
// Since we just wrote, we sleep.
|
||||
// TODO this needs a new mechanism for sleeping because this can
|
||||
// delay shutdown/stop if polling is long.
|
||||
this_thread::sleep_for(sleep_time);
|
||||
}
|
||||
|
||||
spdlog::debug("Persistent storage ending normally");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline int8_t read_and_check(istream& input_stream, const char header[],
|
||||
char buffer[], size_t count) {
|
||||
if (!input_stream.read(buffer, count)) {
|
||||
spdlog::warn("Unable to read header from persistence file stream");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (memcmp(header, buffer, count) != 0) {
|
||||
spdlog::warn("Header does not match expected in persistence file ");
|
||||
return -2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t pstorage_read(istream& input_stream,
|
||||
const GlueVariablesBinding& bindings)
|
||||
{
|
||||
// Read the file header - we define the file header as a constant that
|
||||
// must be present as the header. We don't allow UTF BOMs here.
|
||||
char header_check[FILE_HEADER_SIZE];
|
||||
if (read_and_check(input_stream, FILE_HEADER, header_check, FILE_HEADER_SIZE) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check endianness of the written file
|
||||
char endianness_expected[2] = { IS_BIG_ENDIAN, '\n'};
|
||||
char endianness_check[2];
|
||||
if (read_and_check(input_stream, endianness_expected, header_check, 2) != 0) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
// Now we know that the format is right, so read in the rest. We read
|
||||
// variable by variable so that we can assign into the right value.
|
||||
for (uint16_t index(0); index < bindings.size; ++index) {
|
||||
const GlueVariable& glue = bindings.glue_variables[index];
|
||||
|
||||
if (glue.dir != IECLDT_MEM) {
|
||||
// We only care about items that are stored in memory
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t num_bytes;
|
||||
switch (glue.size) {
|
||||
case IECLST_BIT:
|
||||
num_bytes = 1;
|
||||
break;
|
||||
case IECLST_BYTE:
|
||||
num_bytes = 1;
|
||||
break;
|
||||
case IECLST_WORD:
|
||||
num_bytes = 2;
|
||||
break;
|
||||
case IECLST_DOUBLEWORD:
|
||||
num_bytes = 4;
|
||||
break;
|
||||
case IECLST_LONGWORD:
|
||||
num_bytes = 8;
|
||||
break;
|
||||
default:
|
||||
spdlog::error("Unexpected glue variable type {}", glue.size);
|
||||
return -3;
|
||||
}
|
||||
|
||||
// Read the required number of bytes from the stream
|
||||
// 8 here is the maximum buffer size that we need based on
|
||||
// the types that we support.
|
||||
char buffer[8];
|
||||
if (!input_stream.read(buffer, num_bytes)) {
|
||||
spdlog::error("Persistent storage file too short; partially read");
|
||||
return -4;
|
||||
}
|
||||
|
||||
// Assign the value into the glue value. The value is ether a simple
|
||||
// value or a group of booleans.
|
||||
// We don't actually care what the contents are - we just populate as
|
||||
// though they are raw bytes
|
||||
memcpy(glue.value, buffer, num_bytes);
|
||||
}
|
||||
|
||||
spdlog::info("Initialized from persistent storage");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pstorage_service_init(const GlueVariablesBinding& binding) {
|
||||
ifstream stream("persistent.file", ios::binary);
|
||||
if (!stream) {
|
||||
spdlog::info("Skipped load persistence because file cannot be read.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = pstorage_read(stream, binding);
|
||||
spdlog::info("Storage read completed with result {}", result);
|
||||
}
|
||||
|
||||
void pstorage_service_finalize(const GlueVariablesBinding& binding) {
|
||||
// We don't current do anything on finalize (although we probably should)
|
||||
}
|
||||
|
||||
void pstorage_service_run(const GlueVariablesBinding& binding,
|
||||
volatile bool& run, const char* config) {
|
||||
|
||||
// We don't allow a poll duration of less than one second otherwise
|
||||
// that can have detrimental effects on performance
|
||||
int duration_seconds = max(1, atoi(config));
|
||||
auto create_stream = []() { return new ofstream("persistent.file", ios::binary); };
|
||||
pstorage_run(binding, run, chrono::milliseconds(duration_seconds * 1000), create_stream);
|
||||
}
|
||||
|
||||
/** @}*/
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef CORE_PSTORAGE_H_
|
||||
#define CORE_PSTORAGE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
|
||||
class GlueVariablesBinding;
|
||||
|
||||
/// @brief Initialize persistent storage when the runtime starts.
|
||||
///
|
||||
/// @param glue_variables The glue variables that may be bound into this
|
||||
/// server.
|
||||
void pstorage_service_init(const GlueVariablesBinding& binding);
|
||||
|
||||
/// @brief Finalize persistent storage after the runtime completes.
|
||||
///
|
||||
/// @param glue_variables The glue variables that may be bound into this
|
||||
/// server.
|
||||
void pstorage_service_finalize(const GlueVariablesBinding& binding);
|
||||
|
||||
/// @brief Start the persistent storage server.
|
||||
///
|
||||
/// @param glue_variables The glue variables that may be bound into this
|
||||
/// server.
|
||||
/// @param run A signal for running this server. This server terminates when
|
||||
/// this signal is false.
|
||||
/// @param config The custom configuration for this service.
|
||||
void pstorage_service_run(const GlueVariablesBinding& binding,
|
||||
volatile bool& run, const char* config);
|
||||
|
||||
/// @brief Reads the contents from the input stream into OpenPLC internal
|
||||
/// buffers. The input stream must represent a file that was previously
|
||||
/// written to persistent storage for the same project.
|
||||
///
|
||||
/// This function does not lock buffer access and therefore it is only safe
|
||||
/// to call before the PLC loop begins (during bootstrap).
|
||||
///
|
||||
/// @note This is defined here so that we can access it from the unit tests.
|
||||
///
|
||||
/// @param input_stream The stream to read from. This must be opened for
|
||||
/// binary read.
|
||||
/// @param bindings The glue bindings to populate.
|
||||
/// @return Zero on success, otherwise non-zero. This function may fail
|
||||
/// part way through. Failure does not mean no variables have been set.
|
||||
std::int8_t pstorage_read(std::istream& input_stream,
|
||||
const GlueVariablesBinding& bindings);
|
||||
|
||||
/// @brief Run the persistent storage loop.
|
||||
///
|
||||
/// @note This is defined here so that we can access it from the unit tests.
|
||||
///
|
||||
/// @param bindings The glue bindings to use.
|
||||
/// @param run A flag that indicates if we should terminate the process.
|
||||
/// @param sleep_time How long to wait between each write cycle.
|
||||
/// @return Zero on success, otherwise non-zero. This function may fail
|
||||
/// part way through. Failure does not mean no variables have been set.
|
||||
std::int8_t pstorage_run(const GlueVariablesBinding& bindings,
|
||||
volatile bool& run,
|
||||
std::chrono::milliseconds sleep_time,
|
||||
std::function<std::ostream*(void)> stream_fn);
|
||||
|
||||
/** @}*/
|
||||
|
||||
#endif // CORE_PSTORAGE_H_
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#include <algorithm>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "service_definition.h"
|
||||
#include "../glue.h"
|
||||
#include "../ladder.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void null_handler(const GlueVariablesBinding& binding) {}
|
||||
ServiceInitFunction null_init_fn(null_handler);
|
||||
ServiceFinalizeFunction null_finalize_fn(null_handler);
|
||||
|
||||
ServiceDefinition::ServiceDefinition(const char* name,
|
||||
ServiceStartFunction& start_fn) :
|
||||
name(name),
|
||||
init_fn(null_init_fn),
|
||||
finalize_fn(null_finalize_fn),
|
||||
start_fn(start_fn),
|
||||
running(false),
|
||||
thread(0),
|
||||
config_buffer()
|
||||
{}
|
||||
|
||||
ServiceDefinition::ServiceDefinition(const char* name,
|
||||
ServiceStartFunction& start_fn,
|
||||
ServiceInitFunction& init_fn) :
|
||||
name(name),
|
||||
start_fn(start_fn),
|
||||
init_fn(init_fn),
|
||||
finalize_fn(null_finalize_fn),
|
||||
running(false),
|
||||
thread(0),
|
||||
config_buffer()
|
||||
{}
|
||||
|
||||
ServiceDefinition::ServiceDefinition(const char* name,
|
||||
ServiceStartFunction& start_fn,
|
||||
ServiceInitFunction& init_fn,
|
||||
ServiceFinalizeFunction& finalize_fn) :
|
||||
name(name),
|
||||
start_fn(start_fn),
|
||||
init_fn(init_fn),
|
||||
finalize_fn(finalize_fn),
|
||||
running(false),
|
||||
thread(0),
|
||||
config_buffer()
|
||||
{}
|
||||
|
||||
void ServiceDefinition::initialize() {
|
||||
GlueVariablesBinding bindings(&bufferLock, OPLCGLUE_GLUE_SIZE, oplc_glue_vars);
|
||||
this->init_fn(bindings);
|
||||
}
|
||||
|
||||
void ServiceDefinition::finalize() {
|
||||
GlueVariablesBinding bindings(&bufferLock, OPLCGLUE_GLUE_SIZE, oplc_glue_vars);
|
||||
this->finalize_fn(bindings);
|
||||
}
|
||||
|
||||
void ServiceDefinition::start(const char* config) {
|
||||
// TODO there is a race condition here in creating the thread. This race
|
||||
// condition is old so I'm not trying to solve it now.
|
||||
if (this->running) {
|
||||
spdlog::debug("{} cannot start because it is running.", this->name);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t config_len = strlen(config);
|
||||
if (config_len > MAX_INTERACTIVE_CONFIG_SIZE - 1) {
|
||||
spdlog::warn("{} cannot be started because config is longer than {}.", this->name, MAX_INTERACTIVE_CONFIG_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the configuration information into our configuration buffer
|
||||
strncpy(this->config_buffer, config, min(config_len, MAX_INTERACTIVE_CONFIG_SIZE));
|
||||
|
||||
spdlog::info("Starting service {}", this->name);
|
||||
|
||||
this->running = true;
|
||||
pthread_create(&this->thread, NULL, &ServiceDefinition::run_service, this);
|
||||
pthread_setname_np(this->thread, this->name);
|
||||
}
|
||||
|
||||
void ServiceDefinition::stop() {
|
||||
// TODO there is a threading issue here with access to the thread
|
||||
// and detecting if the service is running.
|
||||
if (this->running) {
|
||||
spdlog::info("Stopping service {}", this->name);
|
||||
this->running = false;
|
||||
pthread_join(this->thread, nullptr);
|
||||
} else {
|
||||
spdlog::debug("Service {} was not running", this->name);
|
||||
}
|
||||
}
|
||||
|
||||
void* ServiceDefinition::run_service(void* user_data) {
|
||||
auto service = reinterpret_cast<ServiceDefinition*>(user_data);
|
||||
|
||||
GlueVariablesBinding bindings(&bufferLock, OPLCGLUE_GLUE_SIZE, oplc_glue_vars);
|
||||
// Start the service. This will not return until the service completes
|
||||
// which is normally because the running flag was set to false.
|
||||
service->start_fn(bindings, service->running, service->config_buffer);
|
||||
|
||||
service->running = false;
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef CORE_SERVICE_SERVICE_DEFINITION_H_
|
||||
#define CORE_SERVICE_SERVICE_DEFINITION_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <pthread.h>
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
|
||||
struct GlueVariablesBinding;
|
||||
|
||||
const std::size_t MAX_INTERACTIVE_CONFIG_SIZE(1024);
|
||||
|
||||
typedef std::function<void(const GlueVariablesBinding& binding)> ServiceInitFunction;
|
||||
typedef std::function<void(const GlueVariablesBinding& binding)> ServiceFinalizeFunction;
|
||||
typedef std::function<void(const GlueVariablesBinding& binding, volatile bool& run, const char* config)> ServiceStartFunction;
|
||||
|
||||
/// A service is the primary extension point for adding support for new
|
||||
/// protocols or hardware specific capabilities. You should not implement
|
||||
/// your service in this class. Rather, this is how the runtime learns
|
||||
/// about your service and you provide to the service definition a set
|
||||
/// of functions that that your service uses.
|
||||
///
|
||||
/// All services run in their own thread and this mechanism is repsonsible
|
||||
/// for creating that thread. This further means that each service, must
|
||||
/// keep only a very short lock on the glue variables so that it cannot
|
||||
/// prevent the main PLC loop from accessing the variables.
|
||||
///
|
||||
/// @note There is presently no way to pass state from init to finalize
|
||||
/// or from start to stop. That's only because we haven't had such a need
|
||||
/// yet. If that comes up, then we'll add that.
|
||||
class ServiceDefinition final {
|
||||
public:
|
||||
/// Initialize a new instance of a service definition that can be started
|
||||
/// and stopped but does not participate in initialize or finalize.
|
||||
/// @param name The unique name of this service.
|
||||
/// @param start_fn A function to start the service.
|
||||
ServiceDefinition(const char* name, ServiceStartFunction& start_fn);
|
||||
|
||||
/// Initialize a new instance of a service definition that can be started
|
||||
/// and stopped and participates in initialize.
|
||||
/// @param name The unique name of this service.
|
||||
/// @param start_fn A function to start the service.
|
||||
/// @param init_fn A function to run when the runtime initializes.
|
||||
ServiceDefinition(const char* name, ServiceStartFunction& start_fn,
|
||||
ServiceInitFunction& init_fn);
|
||||
|
||||
/// Initialize a new instance of a service that participates in all
|
||||
/// lifecycle events (initialize, finalize, start, stop).
|
||||
/// @param name The unique name of this service.
|
||||
/// @param start_fn A function to start the service.
|
||||
/// @param init_fn A function to run when the runtime initializes.
|
||||
/// @param finalize_fn A function to run when the runtime finalizes.
|
||||
ServiceDefinition(const char* name, ServiceStartFunction& start_fn,
|
||||
ServiceInitFunction& init_fn,
|
||||
ServiceFinalizeFunction& finalize_fn);
|
||||
|
||||
/// Lifecycle method for when the runtime starts. This is called for
|
||||
/// the service before the runtime loop beings. This does not mean
|
||||
/// that the service will or will not be started later.
|
||||
void initialize();
|
||||
/// Lifecycle method for when the runtime finalizes. This is called
|
||||
/// after the runtime loop stops.
|
||||
void finalize();
|
||||
|
||||
/// Lifecycle method for when this service has been started on demand.
|
||||
void start(const char* config);
|
||||
/// Lifecycle method for when this service has been stopped on demand.
|
||||
void stop();
|
||||
|
||||
/// Get the descriptive identifier for this service type.
|
||||
const char* id() const { return this->name; }
|
||||
|
||||
/// Get the configuration information associated with starting this
|
||||
/// service.
|
||||
const char* config() const { return this->config_buffer; }
|
||||
|
||||
private:
|
||||
// Hide the copy constructor
|
||||
ServiceDefinition(ServiceDefinition &);
|
||||
static void* run_service(void* user_data);
|
||||
private:
|
||||
/// The type name of the service.
|
||||
const char* name;
|
||||
/// The function to initialize the service.
|
||||
ServiceInitFunction& init_fn;
|
||||
/// The function to finalize the service.
|
||||
ServiceFinalizeFunction& finalize_fn;
|
||||
/// The function to start the service.
|
||||
ServiceStartFunction& start_fn;
|
||||
/// Is the service running.
|
||||
volatile bool running;
|
||||
/// The thread the service is running on.
|
||||
pthread_t thread;
|
||||
/// A buffer for holding the configuration information that was part
|
||||
/// of the request to start the service.
|
||||
char config_buffer[MAX_INTERACTIVE_CONFIG_SIZE];
|
||||
};
|
||||
|
||||
/** @}*/
|
||||
|
||||
#endif // CORE_SERVICE_SERVICE_DEFINITION_H_
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "service_definition.h"
|
||||
#include "service_registry.h"
|
||||
#include "pstorage.h"
|
||||
#include "../dnp3s/dnp3.h"
|
||||
|
||||
ServiceInitFunction pstorage_init_fn(pstorage_service_init);
|
||||
ServiceStartFunction pstorage_start_service_fn(pstorage_service_run);
|
||||
ServiceStartFunction dnp3s_start_service_fn(dnp3s_service_run);
|
||||
|
||||
ServiceDefinition* services[] = {
|
||||
new ServiceDefinition("pstorage", pstorage_start_service_fn, pstorage_init_fn),
|
||||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
new ServiceDefinition("dnp3s", dnp3s_start_service_fn),
|
||||
#endif
|
||||
};
|
||||
|
||||
ServiceDefinition* find_service(const char* name) {
|
||||
ServiceDefinition** item = std::find_if(std::begin(services), std::end(services), [name] (ServiceDefinition* def) {
|
||||
return strcmp(def->id(), name) == 0;
|
||||
});
|
||||
|
||||
return (item != std::end(services)) ? *item : nullptr;
|
||||
}
|
||||
|
||||
void stop_services() {
|
||||
std::for_each(std::begin(services), std::end(services), [] (ServiceDefinition* def){
|
||||
def->stop();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef CORE_SERVICE_SERVICE_REGISTRY_H_
|
||||
#define CORE_SERVICE_SERVICE_REGISTRY_H_
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
|
||||
class ServiceDefinition;
|
||||
|
||||
/// Finds the service in the registry by the name of the service.
|
||||
/// @param name The identifier for the service.
|
||||
/// @return The service if found, or nullptr if there is no such service.
|
||||
ServiceDefinition* find_service(const char* name);
|
||||
|
||||
/// Stop all known services.
|
||||
void stop_services();
|
||||
|
||||
/** @}*/
|
||||
|
||||
#endif // CORE_SERVICE_SERVICE_DEFINITION_H_
|
|
@ -28,9 +28,9 @@ endif()
|
|||
|
||||
# This is all of our test files
|
||||
file(GLOB oplctest_SRC *.cpp **/*.cpp)
|
||||
file(GLOB oplc_dnp3_SRC ../core/dnp3s/*.cpp)
|
||||
file(GLOB oplc_core_SRC ../core/pstorage.cpp ../core/dnp3s/*.cpp)
|
||||
|
||||
add_executable(oplc_unit_test ${oplctest_SRC} ${oplc_dnp3_SRC} ../core/glue.cpp )
|
||||
add_executable(oplc_unit_test ${oplctest_SRC} ${oplc_core_SRC} ../core/glue.cpp )
|
||||
|
||||
target_link_libraries(oplc_unit_test ${OPLC_PTHREAD})
|
||||
if (OPLC_DNP3_OUTSTATION)
|
||||
|
|
|
@ -176,7 +176,7 @@ SCENARIO("create_config", "")
|
|||
}
|
||||
}
|
||||
|
||||
SCENARIO("dnp3StartServer", "")
|
||||
SCENARIO("dnp3s_start_server", "")
|
||||
{
|
||||
mutex glue_mutex;
|
||||
|
||||
|
@ -184,11 +184,11 @@ SCENARIO("dnp3StartServer", "")
|
|||
{
|
||||
// Configure this to start and then immediately terminate
|
||||
// the run flag is set to false. This should just return quickly
|
||||
bool run_dnp3(false);
|
||||
volatile bool run_dnp3(false);
|
||||
unique_ptr<istream, std::function<void(istream*)>> cfg_stream(new stringstream(""), [](istream* s) { delete s; });
|
||||
GlueVariablesBinding bindings(&glue_mutex, 0, nullptr);
|
||||
|
||||
dnp3StartServer(20000, cfg_stream, &run_dnp3, bindings);
|
||||
dnp3s_start_server(20000, cfg_stream, run_dnp3, bindings);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
|
||||
#include "catch.hpp"
|
||||
#include "fakeit.hpp"
|
||||
|
||||
#include "glue.h"
|
||||
#include "pstorage.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
#define IS_BIG_ENDIAN (*(uint16_t *)"\0\xff" < 0x100)
|
||||
|
||||
const char VALID_HEADER[] = { (char)137, 'O', 'P', 'L', 'C', 'P', 'S', '\n', 'v', 0, '\n' };
|
||||
|
||||
SCENARIO("pstorage_read", "") {
|
||||
|
||||
mutex glue_mutex;
|
||||
stringstream input_stream;
|
||||
input_stream.write(VALID_HEADER, 11);
|
||||
char endian_header[2] = { IS_BIG_ENDIAN, '\n'};
|
||||
input_stream.write(endian_header, 2);
|
||||
|
||||
GIVEN("simple glue variables") {
|
||||
IEC_LWORD lword_var = 0;
|
||||
IEC_SINT usint_var = 0;
|
||||
IEC_BOOL bool_var = 0;
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 0, 0, IECVT_LWORD, &lword_var },
|
||||
{ IECLDT_MEM, IECLST_BYTE, 0, 0, IECVT_USINT, &usint_var },
|
||||
{ IECLDT_MEM, IECLST_BIT, 0, 0, IECVT_BOOL, &bool_var },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 3, glue_vars);
|
||||
|
||||
WHEN("no data") {
|
||||
input_stream.seekg(0);
|
||||
REQUIRE(pstorage_read(input_stream, bindings) != 0);
|
||||
}
|
||||
|
||||
WHEN("data too short") {
|
||||
input_stream << '1';
|
||||
input_stream.seekg(0);
|
||||
REQUIRE(pstorage_read(input_stream, bindings) != 0);
|
||||
}
|
||||
|
||||
WHEN("data is valid and all zero") {
|
||||
char buffer[6] = {0};
|
||||
input_stream.write(buffer, 6);
|
||||
input_stream.seekg(0);
|
||||
REQUIRE(pstorage_read(input_stream, bindings) == 0);
|
||||
|
||||
REQUIRE(lword_var == 0);
|
||||
REQUIRE(usint_var == 0);
|
||||
REQUIRE(bool_var == 0);
|
||||
}
|
||||
|
||||
WHEN("data is valid and all one") {
|
||||
IEC_LWORD lword_initial_value = 1;
|
||||
IEC_USINT usint_initial_value = 1;
|
||||
char buffer[6] = {0, 0, 0, 0, 1, 1};
|
||||
// We don't (in general) know the endianness to know
|
||||
// the byte order, so we initialize the buffer based on
|
||||
// the actual memory layout
|
||||
memcpy(buffer, &lword_initial_value, 4);
|
||||
|
||||
input_stream.write(buffer, 6);
|
||||
input_stream.seekg(0);
|
||||
REQUIRE(pstorage_read(input_stream, bindings) == 0);
|
||||
|
||||
REQUIRE(lword_var == 1);
|
||||
REQUIRE(usint_var == 1);
|
||||
REQUIRE(bool_var == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("pstorage_run") {
|
||||
mutex glue_mutex;
|
||||
stringstream input_stream;
|
||||
input_stream.write(VALID_HEADER, 11);
|
||||
char endian_header[2] = { IS_BIG_ENDIAN, '\n'};
|
||||
input_stream.write(endian_header, 2);
|
||||
|
||||
GIVEN("glue variables and stream") {
|
||||
IEC_LWORD lword_var = 1;
|
||||
IEC_SINT usint_var = 2;
|
||||
IEC_BOOL bool_var = 1;
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 0, 0, IECVT_LWORD, &lword_var },
|
||||
{ IECLDT_MEM, IECLST_BYTE, 0, 0, IECVT_USINT, &usint_var },
|
||||
{ IECLDT_MEM, IECLST_BIT, 0, 0, IECVT_BOOL, &bool_var },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 3, glue_vars);
|
||||
|
||||
WHEN("write once") {
|
||||
volatile bool run = false;
|
||||
auto timeout = chrono::milliseconds(0);
|
||||
auto create_stream = []() { return new stringstream(); };
|
||||
REQUIRE(pstorage_run(bindings, run, timeout, create_stream) == 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
language: c
|
||||
|
||||
# Setting sudo access to false will let Travis CI use containers
|
||||
# rather than VMs to run the tests. For more details see:
|
||||
# https://docs.travis-ci.com/user/reference/overview/
|
||||
sudo: false
|
||||
|
||||
script:
|
||||
- cd tests
|
||||
- ./unittest.sh
|
||||
- cd ../examples
|
||||
- ./cpptest.sh
|
||||
- git diff --exit-code
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
The "inih" library is distributed under the New BSD license:
|
||||
|
||||
Copyright (c) 2009, Ben Hoyt
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Ben Hoyt nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY BEN HOYT ''AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL BEN HOYT BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,133 @@
|
|||
# inih (INI Not Invented Here)
|
||||
|
||||
[![TravisCI Build](https://travis-ci.org/benhoyt/inih.svg)](https://travis-ci.org/benhoyt/inih)
|
||||
|
||||
**inih (INI Not Invented Here)** is a simple [.INI file](http://en.wikipedia.org/wiki/INI_file) parser written in C. It's only a couple of pages of code, and it was designed to be _small and simple_, so it's good for embedded systems. It's also more or less compatible with Python's [ConfigParser](http://docs.python.org/library/configparser.html) style of .INI files, including RFC 822-style multi-line syntax and `name: value` entries.
|
||||
|
||||
To use it, just give `ini_parse()` an INI file, and it will call a callback for every `name=value` pair parsed, giving you strings for the section, name, and value. It's done this way ("SAX style") because it works well on low-memory embedded systems, but also because it makes for a KISS implementation.
|
||||
|
||||
You can also call `ini_parse_file()` to parse directly from a `FILE*` object, `ini_parse_string()` to parse data from a string, or `ini_parse_stream()` to parse using a custom fgets-style reader function for custom I/O.
|
||||
|
||||
Download a release, browse the source, or read about [how to use inih in a DRY style](http://blog.brush.co.nz/2009/08/xmacros/) with X-Macros.
|
||||
|
||||
|
||||
## Compile-time options ##
|
||||
|
||||
You can control various aspects of inih using preprocessor defines:
|
||||
|
||||
### Syntax options ###
|
||||
|
||||
* **Multi-line entries:** By default, inih supports multi-line entries in the style of Python's ConfigParser. To disable, add `-DINI_ALLOW_MULTILINE=0`.
|
||||
* **UTF-8 BOM:** By default, inih allows a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of INI files. To disable, add `-DINI_ALLOW_BOM=0`.
|
||||
* **Inline comments:** By default, inih allows inline comments with the `;` character. To disable, add `-DINI_ALLOW_INLINE_COMMENTS=0`. You can also specify which character(s) start an inline comment using `INI_INLINE_COMMENT_PREFIXES`.
|
||||
* **Start-of-line comments:** By default, inih allows both `;` and `#` to start a comment at the beginning of a line. You can override this by changing `INI_START_COMMENT_PREFIXES`.
|
||||
* **Allow no value:** By default, inih treats a name with no value (no `=` or `:` on the line) as an error. To allow names with no values, add `-DINI_ALLOW_NO_VALUE=1`, and inih will call your handler function with value set to NULL.
|
||||
|
||||
### Parsing options ###
|
||||
|
||||
* **Stop on first error:** By default, inih keeps parsing the rest of the file after an error. To stop parsing on the first error, add `-DINI_STOP_ON_FIRST_ERROR=1`.
|
||||
* **Report line numbers:** By default, the `ini_handler` callback doesn't receive the line number as a parameter. If you need that, add `-DINI_HANDLER_LINENO=1`.
|
||||
* **Call handler on new section:** By default, inih only calls the handler on each `name=value` pair. To detect new sections (e.g., the INI file has multiple sections with the same name), add `-DINI_CALL_HANDLER_ON_NEW_SECTION=1`. Your handler function will then be called each time a new section is encountered, with `section` set to the new section name but `name` and `value` set to NULL.
|
||||
|
||||
### Memory options ###
|
||||
|
||||
* **Stack vs heap:** By default, inih creates a fixed-sized line buffer on the stack. To allocate on the heap using `malloc` instead, specify `-DINI_USE_STACK=0`.
|
||||
* **Maximum line length:** The default maximum line length (for stack or heap) is 200 bytes. To override this, add something like `-DINI_MAX_LINE=1000`. Note that `INI_MAX_LINE` must be 3 more than the longest line (due to `\r`, `\n`, and the NUL).
|
||||
* **Allow realloc:** By default when using the heap (`-DINI_USE_STACK=0`), inih allocates a fixed-sized buffer of `INI_INITIAL_ALLOC` bytes. To allow this to grow to `INI_MAX_LINE` bytes, doubling if needed, set `-DINI_ALLOW_REALLOC=1`.
|
||||
* **Initial malloc size:** `INI_INITIAL_ALLOC` specifies the initial malloc size when using the heap. It defaults to 200 bytes.
|
||||
|
||||
## Simple example in C ##
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "../ini.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int version;
|
||||
const char* name;
|
||||
const char* email;
|
||||
} configuration;
|
||||
|
||||
static int handler(void* user, const char* section, const char* name,
|
||||
const char* value)
|
||||
{
|
||||
configuration* pconfig = (configuration*)user;
|
||||
|
||||
#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
|
||||
if (MATCH("protocol", "version")) {
|
||||
pconfig->version = atoi(value);
|
||||
} else if (MATCH("user", "name")) {
|
||||
pconfig->name = strdup(value);
|
||||
} else if (MATCH("user", "email")) {
|
||||
pconfig->email = strdup(value);
|
||||
} else {
|
||||
return 0; /* unknown section/name, error */
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
configuration config;
|
||||
|
||||
if (ini_parse("test.ini", handler, &config) < 0) {
|
||||
printf("Can't load 'test.ini'\n");
|
||||
return 1;
|
||||
}
|
||||
printf("Config loaded from 'test.ini': version=%d, name=%s, email=%s\n",
|
||||
config.version, config.name, config.email);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## C++ example ##
|
||||
|
||||
If you're into C++ and the STL, there is also an easy-to-use [INIReader class](https://github.com/benhoyt/inih/blob/master/cpp/INIReader.h) that stores values in a `map` and lets you `Get()` them:
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
#include "INIReader.h"
|
||||
|
||||
int main()
|
||||
{
|
||||
INIReader reader("../examples/test.ini");
|
||||
|
||||
if (reader.ParseError() < 0) {
|
||||
std::cout << "Can't load 'test.ini'\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Config loaded from 'test.ini': version="
|
||||
<< reader.GetInteger("protocol", "version", -1) << ", name="
|
||||
<< reader.Get("user", "name", "UNKNOWN") << ", email="
|
||||
<< reader.Get("user", "email", "UNKNOWN") << ", pi="
|
||||
<< reader.GetReal("user", "pi", -1) << ", active="
|
||||
<< reader.GetBoolean("user", "active", true) << "\n";
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
This simple C++ API works fine, but it's not very fully-fledged. I'm not planning to work more on the C++ API at the moment, so if you want a bit more power (for example `GetSections()` and `GetFields()` functions), see these forks:
|
||||
|
||||
* https://github.com/Blandinium/inih
|
||||
* https://github.com/OSSystems/inih
|
||||
|
||||
|
||||
## Differences from ConfigParser ##
|
||||
|
||||
Some differences between inih and Python's [ConfigParser](http://docs.python.org/library/configparser.html) standard library module:
|
||||
|
||||
* INI name=value pairs given above any section headers are treated as valid items with no section (section name is an empty string). In ConfigParser having no section is an error.
|
||||
* Line continuations are handled with leading whitespace on continued lines (like ConfigParser). However, instead of concatenating continued lines together, they are treated as separate values for the same key (unlike ConfigParser).
|
||||
|
||||
|
||||
## Platform-specific notes ##
|
||||
|
||||
* Windows/Win32 uses UTF-16 filenames natively, so to handle Unicode paths you need to call `_wfopen()` to open a file and then `ini_parse_file()` to parse it; inih does not include `wchar_t` or Unicode handling.
|
||||
|
||||
## Related links ##
|
||||
|
||||
* [Conan package for inih](https://github.com/mohamedghita/conan-inih) (Conan is a C/C++ package manager)
|
|
@ -0,0 +1,108 @@
|
|||
// Read an INI file into easy-to-access name/value pairs.
|
||||
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Copyright (C) 2009-2019, Ben Hoyt
|
||||
|
||||
// inih and INIReader are released under the New BSD license (see LICENSE.txt).
|
||||
// Go to the project home page for more info:
|
||||
//
|
||||
// https://github.com/benhoyt/inih
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include "../ini.h"
|
||||
#include "INIReader.h"
|
||||
|
||||
using std::string;
|
||||
|
||||
INIReader::INIReader(const string& filename)
|
||||
{
|
||||
_error = ini_parse(filename.c_str(), ValueHandler, this);
|
||||
}
|
||||
|
||||
int INIReader::ParseError() const
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
string INIReader::Get(const string& section, const string& name, const string& default_value) const
|
||||
{
|
||||
string key = MakeKey(section, name);
|
||||
// Use _values.find() here instead of _values.at() to support pre C++11 compilers
|
||||
return _values.count(key) ? _values.find(key)->second : default_value;
|
||||
}
|
||||
|
||||
string INIReader::GetString(const string& section, const string& name, const string& default_value) const
|
||||
{
|
||||
const string str = Get(section, name, "");
|
||||
return str.empty() ? default_value : str;
|
||||
}
|
||||
|
||||
long INIReader::GetInteger(const string& section, const string& name, long default_value) const
|
||||
{
|
||||
string valstr = Get(section, name, "");
|
||||
const char* value = valstr.c_str();
|
||||
char* end;
|
||||
// This parses "1234" (decimal) and also "0x4D2" (hex)
|
||||
long n = strtol(value, &end, 0);
|
||||
return end > value ? n : default_value;
|
||||
}
|
||||
|
||||
double INIReader::GetReal(const string& section, const string& name, double default_value) const
|
||||
{
|
||||
string valstr = Get(section, name, "");
|
||||
const char* value = valstr.c_str();
|
||||
char* end;
|
||||
double n = strtod(value, &end);
|
||||
return end > value ? n : default_value;
|
||||
}
|
||||
|
||||
bool INIReader::GetBoolean(const string& section, const string& name, bool default_value) const
|
||||
{
|
||||
string valstr = Get(section, name, "");
|
||||
// Convert to lower case to make string comparisons case-insensitive
|
||||
std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower);
|
||||
if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1")
|
||||
return true;
|
||||
else if (valstr == "false" || valstr == "no" || valstr == "off" || valstr == "0")
|
||||
return false;
|
||||
else
|
||||
return default_value;
|
||||
}
|
||||
|
||||
bool INIReader::HasSection(const string& section) const
|
||||
{
|
||||
const string key = MakeKey(section, "");
|
||||
std::map<string, string>::const_iterator pos = _values.lower_bound(key);
|
||||
if (pos == _values.end())
|
||||
return false;
|
||||
// Does the key at the lower_bound pos start with "section"?
|
||||
return pos->first.compare(0, key.length(), key) == 0;
|
||||
}
|
||||
|
||||
bool INIReader::HasValue(const string& section, const string& name) const
|
||||
{
|
||||
string key = MakeKey(section, name);
|
||||
return _values.count(key);
|
||||
}
|
||||
|
||||
string INIReader::MakeKey(const string& section, const string& name)
|
||||
{
|
||||
string key = section + "=" + name;
|
||||
// Convert to lower case to make section/name lookups case-insensitive
|
||||
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
|
||||
return key;
|
||||
}
|
||||
|
||||
int INIReader::ValueHandler(void* user, const char* section, const char* name,
|
||||
const char* value)
|
||||
{
|
||||
INIReader* reader = static_cast<INIReader*>(user);
|
||||
string key = MakeKey(section, name);
|
||||
if (reader->_values[key].size() > 0)
|
||||
reader->_values[key] += "\n";
|
||||
reader->_values[key] += value;
|
||||
return 1;
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// Read an INI file into easy-to-access name/value pairs.
|
||||
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Copyright (C) 2009-2019, Ben Hoyt
|
||||
|
||||
// inih and INIReader are released under the New BSD license (see LICENSE.txt).
|
||||
// Go to the project home page for more info:
|
||||
//
|
||||
// https://github.com/benhoyt/inih
|
||||
|
||||
#ifndef __INIREADER_H__
|
||||
#define __INIREADER_H__
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
// Read an INI file into easy-to-access name/value pairs. (Note that I've gone
|
||||
// for simplicity here rather than speed, but it should be pretty decent.)
|
||||
class INIReader
|
||||
{
|
||||
public:
|
||||
// Construct INIReader and parse given filename. See ini.h for more info
|
||||
// about the parsing.
|
||||
explicit INIReader(const std::string& filename);
|
||||
|
||||
// Return the result of ini_parse(), i.e., 0 on success, line number of
|
||||
// first error on parse error, or -1 on file open error.
|
||||
int ParseError() const;
|
||||
|
||||
// Get a string value from INI file, returning default_value if not found.
|
||||
std::string Get(const std::string& section, const std::string& name,
|
||||
const std::string& default_value) const;
|
||||
|
||||
// Get a string value from INI file, returning default_value if not found,
|
||||
// empty, or contains only whitespace.
|
||||
std::string GetString(const std::string& section, const std::string& name,
|
||||
const std::string& default_value) const;
|
||||
|
||||
// Get an integer (long) value from INI file, returning default_value if
|
||||
// not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2").
|
||||
long GetInteger(const std::string& section, const std::string& name, long default_value) const;
|
||||
|
||||
// Get a real (floating point double) value from INI file, returning
|
||||
// default_value if not found or not a valid floating point value
|
||||
// according to strtod().
|
||||
double GetReal(const std::string& section, const std::string& name, double default_value) const;
|
||||
|
||||
// Get a boolean value from INI file, returning default_value if not found or if
|
||||
// not a valid true/false value. Valid true values are "true", "yes", "on", "1",
|
||||
// and valid false values are "false", "no", "off", "0" (not case sensitive).
|
||||
bool GetBoolean(const std::string& section, const std::string& name, bool default_value) const;
|
||||
|
||||
// Return true if the given section exists (section must contain at least
|
||||
// one name=value pair).
|
||||
bool HasSection(const std::string& section) const;
|
||||
|
||||
// Return true if a value exists with the given section and field names.
|
||||
bool HasValue(const std::string& section, const std::string& name) const;
|
||||
|
||||
private:
|
||||
int _error;
|
||||
std::map<std::string, std::string> _values;
|
||||
static std::string MakeKey(const std::string& section, const std::string& name);
|
||||
static int ValueHandler(void* user, const char* section, const char* name,
|
||||
const char* value);
|
||||
};
|
||||
|
||||
#endif // __INIREADER_H__
|
|
@ -0,0 +1,25 @@
|
|||
// Example that shows simple usage of the INIReader class
|
||||
|
||||
#include <iostream>
|
||||
#include "../cpp/INIReader.h"
|
||||
|
||||
int main()
|
||||
{
|
||||
INIReader reader("../examples/test.ini");
|
||||
|
||||
if (reader.ParseError() < 0) {
|
||||
std::cout << "Can't load 'test.ini'\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Config loaded from 'test.ini': version="
|
||||
<< reader.GetInteger("protocol", "version", -1) << ", name="
|
||||
<< reader.Get("user", "name", "UNKNOWN") << ", email="
|
||||
<< reader.Get("user", "email", "UNKNOWN") << ", pi="
|
||||
<< reader.GetReal("user", "pi", -1) << ", active="
|
||||
<< reader.GetBoolean("user", "active", true) << "\n";
|
||||
std::cout << "Has values: user.name=" << reader.HasValue("user", "name")
|
||||
<< ", user.nose=" << reader.HasValue("user", "nose") << "\n";
|
||||
std::cout << "Has sections: user=" << reader.HasSection("user")
|
||||
<< ", fizz=" << reader.HasSection("fizz") << "\n";
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// CFG(section, name, default)
|
||||
|
||||
CFG(protocol, version, "0")
|
||||
|
||||
CFG(user, name, "Fatty Lumpkin")
|
||||
CFG(user, email, "fatty@lumpkin.com")
|
||||
|
||||
#undef CFG
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
g++ INIReaderExample.cpp ../cpp/INIReader.cpp ../ini.c -o INIReaderExample
|
||||
./INIReaderExample > cpptest.txt
|
||||
rm INIReaderExample
|
|
@ -0,0 +1,3 @@
|
|||
Config loaded from 'test.ini': version=6, name=Bob Smith, email=bob@smith.com, pi=3.14159, active=1
|
||||
Has values: user.name=1, user.nose=0
|
||||
Has sections: user=1, fizz=0
|
|
@ -0,0 +1,40 @@
|
|||
/* ini.h example that simply dumps an INI file without comments */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../ini.h"
|
||||
|
||||
static int dumper(void* user, const char* section, const char* name,
|
||||
const char* value)
|
||||
{
|
||||
static char prev_section[50] = "";
|
||||
|
||||
if (strcmp(section, prev_section)) {
|
||||
printf("%s[%s]\n", (prev_section[0] ? "\n" : ""), section);
|
||||
strncpy(prev_section, section, sizeof(prev_section));
|
||||
prev_section[sizeof(prev_section) - 1] = '\0';
|
||||
}
|
||||
printf("%s = %s\n", name, value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
int error;
|
||||
|
||||
if (argc <= 1) {
|
||||
printf("Usage: ini_dump filename.ini\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
error = ini_parse(argv[1], dumper, NULL);
|
||||
if (error < 0) {
|
||||
printf("Can't read '%s'!\n", argv[1]);
|
||||
return 2;
|
||||
}
|
||||
else if (error) {
|
||||
printf("Bad config file (first error on line %d)!\n", error);
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/* Example: parse a simple configuration file */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "../ini.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int version;
|
||||
const char* name;
|
||||
const char* email;
|
||||
} configuration;
|
||||
|
||||
static int handler(void* user, const char* section, const char* name,
|
||||
const char* value)
|
||||
{
|
||||
configuration* pconfig = (configuration*)user;
|
||||
|
||||
#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
|
||||
if (MATCH("protocol", "version")) {
|
||||
pconfig->version = atoi(value);
|
||||
} else if (MATCH("user", "name")) {
|
||||
pconfig->name = strdup(value);
|
||||
} else if (MATCH("user", "email")) {
|
||||
pconfig->email = strdup(value);
|
||||
} else {
|
||||
return 0; /* unknown section/name, error */
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
configuration config;
|
||||
|
||||
if (ini_parse("test.ini", handler, &config) < 0) {
|
||||
printf("Can't load 'test.ini'\n");
|
||||
return 1;
|
||||
}
|
||||
printf("Config loaded from 'test.ini': version=%d, name=%s, email=%s\n",
|
||||
config.version, config.name, config.email);
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/* Parse a configuration file into a struct using X-Macros */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../ini.h"
|
||||
|
||||
/* define the config struct type */
|
||||
typedef struct {
|
||||
#define CFG(s, n, default) char *s##_##n;
|
||||
#include "config.def"
|
||||
} config;
|
||||
|
||||
/* create one and fill in its default values */
|
||||
config Config = {
|
||||
#define CFG(s, n, default) default,
|
||||
#include "config.def"
|
||||
};
|
||||
|
||||
/* process a line of the INI file, storing valid values into config struct */
|
||||
int handler(void *user, const char *section, const char *name,
|
||||
const char *value)
|
||||
{
|
||||
config *cfg = (config *)user;
|
||||
|
||||
if (0) ;
|
||||
#define CFG(s, n, default) else if (strcmp(section, #s)==0 && \
|
||||
strcmp(name, #n)==0) cfg->s##_##n = strdup(value);
|
||||
#include "config.def"
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* print all the variables in the config, one per line */
|
||||
void dump_config(config *cfg)
|
||||
{
|
||||
#define CFG(s, n, default) printf("%s_%s = %s\n", #s, #n, cfg->s##_##n);
|
||||
#include "config.def"
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (ini_parse("test.ini", handler, &Config) < 0)
|
||||
printf("Can't load 'test.ini', using defaults\n");
|
||||
dump_config(&Config);
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
; Test config file for ini_example.c and INIReaderTest.cpp
|
||||
|
||||
[protocol] ; Protocol configuration
|
||||
version=6 ; IPv6
|
||||
|
||||
[user]
|
||||
name = Bob Smith ; Spaces around '=' are stripped
|
||||
email = bob@smith.com ; And comments (like this) ignored
|
||||
active = true ; Test a boolean
|
||||
pi = 3.14159 ; Test a floating point number
|
|
@ -0,0 +1,19 @@
|
|||
# Simple makefile to build inih as a static library using g++
|
||||
|
||||
SRC = ../ini.c
|
||||
OBJ = $(SRC:.c=.o)
|
||||
OUT = libinih.a
|
||||
INCLUDES = -I..
|
||||
CCFLAGS = -g -O2
|
||||
CC = g++
|
||||
|
||||
default: $(OUT)
|
||||
|
||||
.c.o:
|
||||
$(CC) $(INCLUDES) $(CCFLAGS) $(EXTRACCFLAGS) -c $< -o $@
|
||||
|
||||
$(OUT): $(OBJ)
|
||||
ar rcs $(OUT) $(OBJ) $(EXTRAARFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJ) $(OUT)
|
|
@ -0,0 +1,284 @@
|
|||
/* inih -- simple .INI file parser
|
||||
|
||||
SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
Copyright (C) 2009-2019, Ben Hoyt
|
||||
|
||||
inih is released under the New BSD license (see LICENSE.txt). Go to the project
|
||||
home page for more info:
|
||||
|
||||
https://github.com/benhoyt/inih
|
||||
|
||||
*/
|
||||
|
||||
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ini.h"
|
||||
|
||||
#if !INI_USE_STACK
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
#define MAX_SECTION 50
|
||||
#define MAX_NAME 50
|
||||
|
||||
/* Used by ini_parse_string() to keep track of string parsing state. */
|
||||
typedef struct {
|
||||
const char* ptr;
|
||||
size_t num_left;
|
||||
} ini_parse_string_ctx;
|
||||
|
||||
/* Strip whitespace chars off end of given string, in place. Return s. */
|
||||
static char* rstrip(char* s)
|
||||
{
|
||||
char* p = s + strlen(s);
|
||||
while (p > s && isspace((unsigned char)(*--p)))
|
||||
*p = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Return pointer to first non-whitespace char in given string. */
|
||||
static char* lskip(const char* s)
|
||||
{
|
||||
while (*s && isspace((unsigned char)(*s)))
|
||||
s++;
|
||||
return (char*)s;
|
||||
}
|
||||
|
||||
/* Return pointer to first char (of chars) or inline comment in given string,
|
||||
or pointer to null at end of string if neither found. Inline comment must
|
||||
be prefixed by a whitespace character to register as a comment. */
|
||||
static char* find_chars_or_comment(const char* s, const char* chars)
|
||||
{
|
||||
#if INI_ALLOW_INLINE_COMMENTS
|
||||
int was_space = 0;
|
||||
while (*s && (!chars || !strchr(chars, *s)) &&
|
||||
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
|
||||
was_space = isspace((unsigned char)(*s));
|
||||
s++;
|
||||
}
|
||||
#else
|
||||
while (*s && (!chars || !strchr(chars, *s))) {
|
||||
s++;
|
||||
}
|
||||
#endif
|
||||
return (char*)s;
|
||||
}
|
||||
|
||||
/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
|
||||
static char* strncpy0(char* dest, const char* src, size_t size)
|
||||
{
|
||||
strncpy(dest, src, size - 1);
|
||||
dest[size - 1] = '\0';
|
||||
return dest;
|
||||
}
|
||||
|
||||
/* See documentation in header file. */
|
||||
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
|
||||
void* user)
|
||||
{
|
||||
/* Uses a fair bit of stack (use heap instead if you need to) */
|
||||
#if INI_USE_STACK
|
||||
char line[INI_MAX_LINE];
|
||||
int max_line = INI_MAX_LINE;
|
||||
#else
|
||||
char* line;
|
||||
int max_line = INI_INITIAL_ALLOC;
|
||||
#endif
|
||||
#if INI_ALLOW_REALLOC && !INI_USE_STACK
|
||||
char* new_line;
|
||||
int offset;
|
||||
#endif
|
||||
char section[MAX_SECTION] = "";
|
||||
char prev_name[MAX_NAME] = "";
|
||||
|
||||
char* start;
|
||||
char* end;
|
||||
char* name;
|
||||
char* value;
|
||||
int lineno = 0;
|
||||
int error = 0;
|
||||
|
||||
#if !INI_USE_STACK
|
||||
line = (char*)malloc(INI_INITIAL_ALLOC);
|
||||
if (!line) {
|
||||
return -2;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if INI_HANDLER_LINENO
|
||||
#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
|
||||
#else
|
||||
#define HANDLER(u, s, n, v) handler(u, s, n, v)
|
||||
#endif
|
||||
|
||||
/* Scan through stream line by line */
|
||||
while (reader(line, max_line, stream) != NULL) {
|
||||
#if INI_ALLOW_REALLOC && !INI_USE_STACK
|
||||
offset = strlen(line);
|
||||
while (offset == max_line - 1 && line[offset - 1] != '\n') {
|
||||
max_line *= 2;
|
||||
if (max_line > INI_MAX_LINE)
|
||||
max_line = INI_MAX_LINE;
|
||||
new_line = realloc(line, max_line);
|
||||
if (!new_line) {
|
||||
free(line);
|
||||
return -2;
|
||||
}
|
||||
line = new_line;
|
||||
if (reader(line + offset, max_line - offset, stream) == NULL)
|
||||
break;
|
||||
if (max_line >= INI_MAX_LINE)
|
||||
break;
|
||||
offset += strlen(line + offset);
|
||||
}
|
||||
#endif
|
||||
|
||||
lineno++;
|
||||
|
||||
start = line;
|
||||
#if INI_ALLOW_BOM
|
||||
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
|
||||
(unsigned char)start[1] == 0xBB &&
|
||||
(unsigned char)start[2] == 0xBF) {
|
||||
start += 3;
|
||||
}
|
||||
#endif
|
||||
start = lskip(rstrip(start));
|
||||
|
||||
if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
|
||||
/* Start-of-line comment */
|
||||
}
|
||||
#if INI_ALLOW_MULTILINE
|
||||
else if (*prev_name && *start && start > line) {
|
||||
/* Non-blank line with leading whitespace, treat as continuation
|
||||
of previous name's value (as per Python configparser). */
|
||||
if (!HANDLER(user, section, prev_name, start) && !error)
|
||||
error = lineno;
|
||||
}
|
||||
#endif
|
||||
else if (*start == '[') {
|
||||
/* A "[section]" line */
|
||||
end = find_chars_or_comment(start + 1, "]");
|
||||
if (*end == ']') {
|
||||
*end = '\0';
|
||||
strncpy0(section, start + 1, sizeof(section));
|
||||
*prev_name = '\0';
|
||||
#if INI_CALL_HANDLER_ON_NEW_SECTION
|
||||
if (!HANDLER(user, section, NULL, NULL) && !error)
|
||||
error = lineno;
|
||||
#endif
|
||||
}
|
||||
else if (!error) {
|
||||
/* No ']' found on section line */
|
||||
error = lineno;
|
||||
}
|
||||
}
|
||||
else if (*start) {
|
||||
/* Not a comment, must be a name[=:]value pair */
|
||||
end = find_chars_or_comment(start, "=:");
|
||||
if (*end == '=' || *end == ':') {
|
||||
*end = '\0';
|
||||
name = rstrip(start);
|
||||
value = end + 1;
|
||||
#if INI_ALLOW_INLINE_COMMENTS
|
||||
end = find_chars_or_comment(value, NULL);
|
||||
if (*end)
|
||||
*end = '\0';
|
||||
#endif
|
||||
value = lskip(value);
|
||||
rstrip(value);
|
||||
|
||||
/* Valid name[=:]value pair found, call handler */
|
||||
strncpy0(prev_name, name, sizeof(prev_name));
|
||||
if (!HANDLER(user, section, name, value) && !error)
|
||||
error = lineno;
|
||||
}
|
||||
else if (!error) {
|
||||
/* No '=' or ':' found on name[=:]value line */
|
||||
#if INI_ALLOW_NO_VALUE
|
||||
*end = '\0';
|
||||
name = rstrip(start);
|
||||
if (!HANDLER(user, section, name, NULL) && !error)
|
||||
error = lineno;
|
||||
#else
|
||||
error = lineno;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if INI_STOP_ON_FIRST_ERROR
|
||||
if (error)
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !INI_USE_STACK
|
||||
free(line);
|
||||
#endif
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/* See documentation in header file. */
|
||||
int ini_parse_file(FILE* file, ini_handler handler, void* user)
|
||||
{
|
||||
return ini_parse_stream((ini_reader)fgets, file, handler, user);
|
||||
}
|
||||
|
||||
/* See documentation in header file. */
|
||||
int ini_parse(const char* filename, ini_handler handler, void* user)
|
||||
{
|
||||
FILE* file;
|
||||
int error;
|
||||
|
||||
file = fopen(filename, "r");
|
||||
if (!file)
|
||||
return -1;
|
||||
error = ini_parse_file(file, handler, user);
|
||||
fclose(file);
|
||||
return error;
|
||||
}
|
||||
|
||||
/* An ini_reader function to read the next line from a string buffer. This
|
||||
is the fgets() equivalent used by ini_parse_string(). */
|
||||
static char* ini_reader_string(char* str, int num, void* stream) {
|
||||
ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream;
|
||||
const char* ctx_ptr = ctx->ptr;
|
||||
size_t ctx_num_left = ctx->num_left;
|
||||
char* strp = str;
|
||||
char c;
|
||||
|
||||
if (ctx_num_left == 0 || num < 2)
|
||||
return NULL;
|
||||
|
||||
while (num > 1 && ctx_num_left != 0) {
|
||||
c = *ctx_ptr++;
|
||||
ctx_num_left--;
|
||||
*strp++ = c;
|
||||
if (c == '\n')
|
||||
break;
|
||||
num--;
|
||||
}
|
||||
|
||||
*strp = '\0';
|
||||
ctx->ptr = ctx_ptr;
|
||||
ctx->num_left = ctx_num_left;
|
||||
return str;
|
||||
}
|
||||
|
||||
/* See documentation in header file. */
|
||||
int ini_parse_string(const char* string, ini_handler handler, void* user) {
|
||||
ini_parse_string_ctx ctx;
|
||||
|
||||
ctx.ptr = string;
|
||||
ctx.num_left = strlen(string);
|
||||
return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler,
|
||||
user);
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/* inih -- simple .INI file parser
|
||||
|
||||
SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
Copyright (C) 2009-2019, Ben Hoyt
|
||||
|
||||
inih is released under the New BSD license (see LICENSE.txt). Go to the project
|
||||
home page for more info:
|
||||
|
||||
https://github.com/benhoyt/inih
|
||||
|
||||
*/
|
||||
|
||||
#ifndef __INI_H__
|
||||
#define __INI_H__
|
||||
|
||||
/* Make this header file easier to include in C++ code */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
/* Nonzero if ini_handler callback should accept lineno parameter. */
|
||||
#ifndef INI_HANDLER_LINENO
|
||||
#define INI_HANDLER_LINENO 0
|
||||
#endif
|
||||
|
||||
/* Typedef for prototype of handler function. */
|
||||
#if INI_HANDLER_LINENO
|
||||
typedef int (*ini_handler)(void* user, const char* section,
|
||||
const char* name, const char* value,
|
||||
int lineno);
|
||||
#else
|
||||
typedef int (*ini_handler)(void* user, const char* section,
|
||||
const char* name, const char* value);
|
||||
#endif
|
||||
|
||||
/* Typedef for prototype of fgets-style reader function. */
|
||||
typedef char* (*ini_reader)(char* str, int num, void* stream);
|
||||
|
||||
/* Parse given INI-style file. May have [section]s, name=value pairs
|
||||
(whitespace stripped), and comments starting with ';' (semicolon). Section
|
||||
is "" if name=value pair parsed before any section heading. name:value
|
||||
pairs are also supported as a concession to Python's configparser.
|
||||
|
||||
For each name=value pair parsed, call handler function with given user
|
||||
pointer as well as section, name, and value (data only valid for duration
|
||||
of handler call). Handler should return nonzero on success, zero on error.
|
||||
|
||||
Returns 0 on success, line number of first error on parse error (doesn't
|
||||
stop on first error), -1 on file open error, or -2 on memory allocation
|
||||
error (only when INI_USE_STACK is zero).
|
||||
*/
|
||||
int ini_parse(const char* filename, ini_handler handler, void* user);
|
||||
|
||||
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
|
||||
close the file when it's finished -- the caller must do that. */
|
||||
int ini_parse_file(FILE* file, ini_handler handler, void* user);
|
||||
|
||||
/* Same as ini_parse(), but takes an ini_reader function pointer instead of
|
||||
filename. Used for implementing custom or string-based I/O (see also
|
||||
ini_parse_string). */
|
||||
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
|
||||
void* user);
|
||||
|
||||
/* Same as ini_parse(), but takes a zero-terminated string with the INI data
|
||||
instead of a file. Useful for parsing INI data from a network socket or
|
||||
already in memory. */
|
||||
int ini_parse_string(const char* string, ini_handler handler, void* user);
|
||||
|
||||
/* Nonzero to allow multi-line value parsing, in the style of Python's
|
||||
configparser. If allowed, ini_parse() will call the handler with the same
|
||||
name for each subsequent line parsed. */
|
||||
#ifndef INI_ALLOW_MULTILINE
|
||||
#define INI_ALLOW_MULTILINE 1
|
||||
#endif
|
||||
|
||||
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
|
||||
the file. See https://github.com/benhoyt/inih/issues/21 */
|
||||
#ifndef INI_ALLOW_BOM
|
||||
#define INI_ALLOW_BOM 1
|
||||
#endif
|
||||
|
||||
/* Chars that begin a start-of-line comment. Per Python configparser, allow
|
||||
both ; and # comments at the start of a line by default. */
|
||||
#ifndef INI_START_COMMENT_PREFIXES
|
||||
#define INI_START_COMMENT_PREFIXES ";#"
|
||||
#endif
|
||||
|
||||
/* Nonzero to allow inline comments (with valid inline comment characters
|
||||
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
|
||||
Python 3.2+ configparser behaviour. */
|
||||
#ifndef INI_ALLOW_INLINE_COMMENTS
|
||||
#define INI_ALLOW_INLINE_COMMENTS 1
|
||||
#endif
|
||||
#ifndef INI_INLINE_COMMENT_PREFIXES
|
||||
#define INI_INLINE_COMMENT_PREFIXES ";"
|
||||
#endif
|
||||
|
||||
/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */
|
||||
#ifndef INI_USE_STACK
|
||||
#define INI_USE_STACK 1
|
||||
#endif
|
||||
|
||||
/* Maximum line length for any line in INI file (stack or heap). Note that
|
||||
this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */
|
||||
#ifndef INI_MAX_LINE
|
||||
#define INI_MAX_LINE 200
|
||||
#endif
|
||||
|
||||
/* Nonzero to allow heap line buffer to grow via realloc(), zero for a
|
||||
fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is
|
||||
zero. */
|
||||
#ifndef INI_ALLOW_REALLOC
|
||||
#define INI_ALLOW_REALLOC 0
|
||||
#endif
|
||||
|
||||
/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK
|
||||
is zero. */
|
||||
#ifndef INI_INITIAL_ALLOC
|
||||
#define INI_INITIAL_ALLOC 200
|
||||
#endif
|
||||
|
||||
/* Stop parsing on first error (default is to keep parsing). */
|
||||
#ifndef INI_STOP_ON_FIRST_ERROR
|
||||
#define INI_STOP_ON_FIRST_ERROR 0
|
||||
#endif
|
||||
|
||||
/* Nonzero to call the handler at the start of each new section (with
|
||||
name and value NULL). Default is to only call the handler on
|
||||
each name=value pair. */
|
||||
#ifndef INI_CALL_HANDLER_ON_NEW_SECTION
|
||||
#define INI_CALL_HANDLER_ON_NEW_SECTION 0
|
||||
#endif
|
||||
|
||||
/* Nonzero to allow a name without a value (no '=' or ':' on the line) and
|
||||
call the handler with value NULL in this case. Default is to treat
|
||||
no-value lines as an error. */
|
||||
#ifndef INI_ALLOW_NO_VALUE
|
||||
#define INI_ALLOW_NO_VALUE 0
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __INI_H__ */
|
|
@ -0,0 +1 @@
|
|||
This is an error
|
|
@ -0,0 +1 @@
|
|||
indented
|
|
@ -0,0 +1,5 @@
|
|||
[section1]
|
||||
name1=value1
|
||||
[section2
|
||||
[section3 ; comment ]
|
||||
name2=value2
|
|
@ -0,0 +1,69 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test7=;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
... This is an error;
|
||||
bad_comment.ini: e=0 user=103
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line value;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
multi_line.ini: e=0 user=105
|
||||
... indented;
|
||||
bad_multi.ini: e=0 user=106
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section_list]
|
||||
... section0;
|
||||
... section1;
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=0 user=109
|
|
@ -0,0 +1,67 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [empty]
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test7=;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line value;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
multi_line.ini: e=0 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... [section1]
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section_list]
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,65 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test ; name=value comment;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3 ; only this will be a comment;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345 ; key should be "test;3";
|
||||
... test4=4#5#6 ; '#' only starts a comment at start of line;
|
||||
... test7=; blank value, except if inline comments disabled;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... [section3 ; comment ]
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line value;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith ; comment line 1;
|
||||
multi_line.ini: e=0 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,64 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test; line 3
|
||||
... two=1234; line 4
|
||||
... [ section 2 ]
|
||||
... happy=4; line 8
|
||||
... sad=; line 9
|
||||
... [comment_test]
|
||||
... test1=1;2;3; line 15
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';'; line 16
|
||||
... test;3=345; line 17
|
||||
... test4=4#5#6; line 18
|
||||
... test7=; line 21
|
||||
... test8=; not a comment, needs whitespace before ';'; line 22
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html; line 25
|
||||
... foo=bar; line 26
|
||||
... adams=42; line 27
|
||||
... funny1=with = equals; line 28
|
||||
... funny2=with : colons; line 29
|
||||
... funny3=two = equals; line 30
|
||||
... funny4=two : colons; line 31
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1; line 2
|
||||
... name2=value2; line 5
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b; line 2
|
||||
... user=parse_error; line 3
|
||||
... c=d; line 4
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc; line 2
|
||||
... multi=this is a; line 3
|
||||
... multi=multi-line value; line 4
|
||||
... single2=xyz; line 5
|
||||
... [section2]
|
||||
... multi=a; line 7
|
||||
... multi=b; line 8
|
||||
... multi=c; line 9
|
||||
... [section3]
|
||||
... single=ghi; line 11
|
||||
... multi=the quick; line 12
|
||||
... multi=brown fox; line 13
|
||||
... name=bob smith; line 14
|
||||
multi_line.ini: e=0 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value; line 2
|
||||
... key“=value“; line 3
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc; line 2
|
||||
... single2=xyz; line 3
|
||||
... single1=def; line 5
|
||||
... single2=qrs; line 6
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0; line 6
|
||||
... [section1]
|
||||
... key1=val1; line 9
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,64 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test7=;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line value;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
multi_line.ini: e=0 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,68 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this;
|
||||
... test2=needs whitespace b;
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test4=only starts a comm;
|
||||
... test7=;
|
||||
... test8=; not a comm;
|
||||
... [colon_tests]
|
||||
... Content-Type=text/;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equ;
|
||||
... funny2=with : col;
|
||||
... funny3=two = equa;
|
||||
... funny4=two : colo;
|
||||
normal.ini: e=2 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
... name=comment line 1;
|
||||
... name=comment line 2;
|
||||
multi_line.ini: e=5 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,64 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test7=;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line value;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
multi_line.ini: e=0 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,68 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this;
|
||||
... test2=needs whitespace b;
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test4=only starts a comm;
|
||||
... test7=;
|
||||
... test8=; not a comm;
|
||||
... [colon_tests]
|
||||
... Content-Type=text/;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equ;
|
||||
... funny2=with : col;
|
||||
... funny3=two = equa;
|
||||
... funny4=two : colo;
|
||||
normal.ini: e=2 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
... name=comment line 1;
|
||||
... name=comment line 2;
|
||||
multi_line.ini: e=5 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,21 @@
|
|||
empty string: e=0 user=0
|
||||
... [section]
|
||||
... foo=bar;
|
||||
... bazz=buzz quxx;
|
||||
basic: e=0 user=101
|
||||
... [section]
|
||||
... hello=world;
|
||||
... forty_two=42;
|
||||
crlf: e=0 user=102
|
||||
... [sec]
|
||||
... foo=0123456789012;
|
||||
... bar=4321;
|
||||
long line: e=3 user=103
|
||||
... [sec]
|
||||
... foo=0123456789012;
|
||||
... bix=1234;
|
||||
long continued: e=0 user=104
|
||||
... [s]
|
||||
... a=1;
|
||||
... c=3;
|
||||
error: e=3 user=105
|
|
@ -0,0 +1,64 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test7=;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line value;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
multi_line.ini: e=0 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,68 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this;
|
||||
... test2=needs whitespace b;
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test4=only starts a comm;
|
||||
... test7=;
|
||||
... test8=; not a comm;
|
||||
... [colon_tests]
|
||||
... Content-Type=text/;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equ;
|
||||
... funny2=with : col;
|
||||
... funny3=two = equa;
|
||||
... funny4=two : colo;
|
||||
normal.ini: e=2 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
... name=comment line 1;
|
||||
... name=comment line 2;
|
||||
multi_line.ini: e=5 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,60 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test7=;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
... name2=value2;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
... c=d;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... name=bob smith;
|
||||
multi_line.ini: e=4 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
... [section0]
|
||||
... key0=val0;
|
||||
... [section1]
|
||||
... key1=val1;
|
||||
no_value.ini: e=2 user=109
|
|
@ -0,0 +1,58 @@
|
|||
no_file.ini: e=-1 user=0
|
||||
... [section1]
|
||||
... one=This is a test;
|
||||
... two=1234;
|
||||
... [ section 2 ]
|
||||
... happy=4;
|
||||
... sad=;
|
||||
... [comment_test]
|
||||
... test1=1;2;3;
|
||||
... test2=2;3;4;this won't be a comment, needs whitespace before ';';
|
||||
... test;3=345;
|
||||
... test4=4#5#6;
|
||||
... test7=;
|
||||
... test8=; not a comment, needs whitespace before ';';
|
||||
... [colon_tests]
|
||||
... Content-Type=text/html;
|
||||
... foo=bar;
|
||||
... adams=42;
|
||||
... funny1=with = equals;
|
||||
... funny2=with : colons;
|
||||
... funny3=two = equals;
|
||||
... funny4=two : colons;
|
||||
normal.ini: e=0 user=101
|
||||
... [section1]
|
||||
... name1=value1;
|
||||
bad_section.ini: e=3 user=102
|
||||
bad_comment.ini: e=1 user=102
|
||||
... [section]
|
||||
... a=b;
|
||||
... user=parse_error;
|
||||
user_error.ini: e=3 user=104
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... multi=this is a;
|
||||
... multi=multi-line value;
|
||||
... single2=xyz;
|
||||
... [section2]
|
||||
... multi=a;
|
||||
... multi=b;
|
||||
... multi=c;
|
||||
... [section3]
|
||||
... single=ghi;
|
||||
... multi=the quick;
|
||||
... multi=brown fox;
|
||||
... name=bob smith;
|
||||
multi_line.ini: e=0 user=105
|
||||
bad_multi.ini: e=1 user=105
|
||||
... [bom_section]
|
||||
... bom_name=bom_value;
|
||||
... key“=value“;
|
||||
bom.ini: e=0 user=107
|
||||
... [section1]
|
||||
... single1=abc;
|
||||
... single2=xyz;
|
||||
... single1=def;
|
||||
... single2=qrs;
|
||||
duplicate_sections.ini: e=0 user=108
|
||||
no_value.ini: e=2 user=108
|
|
@ -0,0 +1,21 @@
|
|||
empty string: e=0 user=0
|
||||
... [section]
|
||||
... foo=bar;
|
||||
... bazz=buzz quxx;
|
||||
basic: e=0 user=101
|
||||
... [section]
|
||||
... hello=world;
|
||||
... forty_two=42;
|
||||
crlf: e=0 user=102
|
||||
... [sec]
|
||||
... foo=0123456789012;
|
||||
... bar=4321;
|
||||
long line: e=3 user=103
|
||||
... [sec]
|
||||
... foo=0123456789012;
|
||||
... bix=1234;
|
||||
long continued: e=0 user=104
|
||||
... [s]
|
||||
... a=1;
|
||||
... c=3;
|
||||
error: e=3 user=105
|
|
@ -0,0 +1,3 @@
|
|||
[bom_section]
|
||||
bom_name=bom_value
|
||||
key“ = value“
|
|
@ -0,0 +1,6 @@
|
|||
[section1]
|
||||
single1 = abc
|
||||
single2 = xyz
|
||||
[section1]
|
||||
single1 = def
|
||||
single2 = qrs
|
|
@ -0,0 +1,15 @@
|
|||
[section1]
|
||||
single1 = abc
|
||||
multi = this is a
|
||||
multi-line value
|
||||
single2 = xyz
|
||||
[section2]
|
||||
multi = a
|
||||
b
|
||||
c
|
||||
[section3]
|
||||
single: ghi
|
||||
multi: the quick
|
||||
brown fox
|
||||
name = bob smith ; comment line 1
|
||||
; comment line 2
|
|
@ -0,0 +1,9 @@
|
|||
[section_list]
|
||||
section0
|
||||
section1
|
||||
|
||||
[section0]
|
||||
key0=val0
|
||||
|
||||
[section1]
|
||||
key1=val1
|
|
@ -0,0 +1,31 @@
|
|||
; This is an INI file
|
||||
[section1] ; section comment
|
||||
one=This is a test ; name=value comment
|
||||
two = 1234
|
||||
; x=y
|
||||
|
||||
[ section 2 ]
|
||||
happy = 4
|
||||
sad =
|
||||
|
||||
[empty]
|
||||
; do nothing
|
||||
|
||||
[comment_test]
|
||||
test1 = 1;2;3 ; only this will be a comment
|
||||
test2 = 2;3;4;this won't be a comment, needs whitespace before ';'
|
||||
test;3 = 345 ; key should be "test;3"
|
||||
test4 = 4#5#6 ; '#' only starts a comment at start of line
|
||||
#test5 = 567 ; entire line commented
|
||||
# test6 = 678 ; entire line commented, except in MULTILINE mode
|
||||
test7 = ; blank value, except if inline comments disabled
|
||||
test8 =; not a comment, needs whitespace before ';'
|
||||
|
||||
[colon_tests]
|
||||
Content-Type: text/html
|
||||
foo:bar
|
||||
adams : 42
|
||||
funny1 : with = equals
|
||||
funny2 = with : colons
|
||||
funny3 = two = equals
|
||||
funny4 : two : colons
|
|
@ -0,0 +1,13 @@
|
|||
@call tcc ..\ini.c -I..\ -run unittest.c > baseline_multi.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_MAX_LINE=20 -run unittest.c > baseline_multi_max_line.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_ALLOW_MULTILINE=0 -run unittest.c > baseline_single.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_ALLOW_INLINE_COMMENTS=0 -run unittest.c > baseline_disallow_inline_comments.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_STOP_ON_FIRST_ERROR=1 -run unittest.c > baseline_stop_on_first_error.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_HANDLER_LINENO=1 -run unittest.c > baseline_handler_lineno.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_USE_STACK=0 -run unittest.c > baseline_heap.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_USE_STACK=0 -DINI_MAX_LINE=20 -DINI_INITIAL_ALLOC=20 -run unittest.c > baseline_heap_max_line.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_USE_STACK=0 -DINI_ALLOW_REALLOC=1 -DINI_INITIAL_ALLOC=5 -run unittest.c > baseline_heap_realloc.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_USE_STACK=0 -DINI_MAX_LINE=20 -DINI_ALLOW_REALLOC=1 -DINI_INITIAL_ALLOC=5 -run unittest.c > baseline_heap_realloc_max_line.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_USE_STACK=0 -DINI_MAX_LINE=20 -DINI_INITIAL_ALLOC=20 -run unittest.c > baseline_heap_string.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_CALL_HANDLER_ON_NEW_SECTION=1 -run unittest.c > baseline_call_handler_on_new_section.txt
|
||||
@call tcc ..\ini.c -I..\ -DINI_ALLOW_NO_VALUE=1 -run unittest.c > baseline_allow_no_value.txt
|
|
@ -0,0 +1,71 @@
|
|||
/* inih -- unit tests
|
||||
|
||||
This works simply by dumping a bunch of info to standard output, which is
|
||||
redirected to an output file (baseline_*.txt) and checked into the Subversion
|
||||
repository. This baseline file is the test output, so the idea is to check it
|
||||
once, and if it changes -- look at the diff and see which tests failed.
|
||||
|
||||
See unittest.bat and unittest.sh for how to run this (with tcc and gcc,
|
||||
respectively).
|
||||
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "../ini.h"
|
||||
|
||||
int User;
|
||||
char Prev_section[50];
|
||||
|
||||
#if INI_HANDLER_LINENO
|
||||
int dumper(void* user, const char* section, const char* name,
|
||||
const char* value, int lineno)
|
||||
#else
|
||||
int dumper(void* user, const char* section, const char* name,
|
||||
const char* value)
|
||||
#endif
|
||||
{
|
||||
User = *((int*)user);
|
||||
if (!name || strcmp(section, Prev_section)) {
|
||||
printf("... [%s]\n", section);
|
||||
strncpy(Prev_section, section, sizeof(Prev_section));
|
||||
Prev_section[sizeof(Prev_section) - 1] = '\0';
|
||||
}
|
||||
if (!name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#if INI_HANDLER_LINENO
|
||||
printf("... %s%s%s; line %d\n", name, value ? "=" : "", value ? value : "", lineno);
|
||||
#else
|
||||
printf("... %s%s%s;\n", name, value ? "=" : "", value ? value : "");
|
||||
#endif
|
||||
|
||||
return strcmp(name, "user")==0 && strcmp(value, "parse_error")==0 ? 0 : 1;
|
||||
}
|
||||
|
||||
void parse(const char* fname) {
|
||||
static int u = 100;
|
||||
int e;
|
||||
|
||||
*Prev_section = '\0';
|
||||
e = ini_parse(fname, dumper, &u);
|
||||
printf("%s: e=%d user=%d\n", fname, e, User);
|
||||
u++;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
parse("no_file.ini");
|
||||
parse("normal.ini");
|
||||
parse("bad_section.ini");
|
||||
parse("bad_comment.ini");
|
||||
parse("user_error.ini");
|
||||
parse("multi_line.ini");
|
||||
parse("bad_multi.ini");
|
||||
parse("bom.ini");
|
||||
parse("duplicate_sections.ini");
|
||||
parse("no_value.ini");
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
gcc ../ini.c unittest.c -o unittest_multi
|
||||
./unittest_multi > baseline_multi.txt
|
||||
rm -f unittest_multi
|
||||
|
||||
gcc ../ini.c -DINI_MAX_LINE=20 unittest.c -o unittest_multi_max_line
|
||||
./unittest_multi_max_line > baseline_multi_max_line.txt
|
||||
rm -f unittest_multi_max_line
|
||||
|
||||
gcc ../ini.c -DINI_ALLOW_MULTILINE=0 unittest.c -o unittest_single
|
||||
./unittest_single > baseline_single.txt
|
||||
rm -f unittest_single
|
||||
|
||||
gcc ../ini.c -DINI_ALLOW_INLINE_COMMENTS=0 unittest.c -o unittest_disallow_inline_comments
|
||||
./unittest_disallow_inline_comments > baseline_disallow_inline_comments.txt
|
||||
rm -f unittest_disallow_inline_comments
|
||||
|
||||
gcc ../ini.c -DINI_STOP_ON_FIRST_ERROR=1 unittest.c -o unittest_stop_on_first_error
|
||||
./unittest_stop_on_first_error > baseline_stop_on_first_error.txt
|
||||
rm -f unittest_stop_on_first_error
|
||||
|
||||
gcc ../ini.c -DINI_HANDLER_LINENO=1 unittest.c -o unittest_handler_lineno
|
||||
./unittest_handler_lineno > baseline_handler_lineno.txt
|
||||
rm -f unittest_handler_lineno
|
||||
|
||||
gcc ../ini.c -DINI_MAX_LINE=20 unittest_string.c -o unittest_string
|
||||
./unittest_string > baseline_string.txt
|
||||
rm -f unittest_string
|
||||
|
||||
gcc ../ini.c -DINI_USE_STACK=0 unittest.c -o unittest_heap
|
||||
./unittest_heap > baseline_heap.txt
|
||||
rm -f unittest_heap
|
||||
|
||||
gcc ../ini.c -DINI_USE_STACK=0 -DINI_MAX_LINE=20 -DINI_INITIAL_ALLOC=20 unittest.c -o unittest_heap_max_line
|
||||
./unittest_heap_max_line > baseline_heap_max_line.txt
|
||||
rm -f unittest_heap_max_line
|
||||
|
||||
gcc ../ini.c -DINI_USE_STACK=0 -DINI_ALLOW_REALLOC=1 -DINI_INITIAL_ALLOC=5 unittest.c -o unittest_heap_realloc
|
||||
./unittest_heap_realloc > baseline_heap_realloc.txt
|
||||
rm -f unittest_heap_realloc
|
||||
|
||||
gcc ../ini.c -DINI_USE_STACK=0 -DINI_MAX_LINE=20 -DINI_ALLOW_REALLOC=1 -DINI_INITIAL_ALLOC=5 unittest.c -o unittest_heap_realloc_max_line
|
||||
./unittest_heap_realloc_max_line > baseline_heap_realloc_max_line.txt
|
||||
rm -f unittest_heap_realloc_max_line
|
||||
|
||||
gcc ../ini.c -DINI_USE_STACK=0 -DINI_MAX_LINE=20 -DINI_INITIAL_ALLOC=20 unittest_string.c -o unittest_heap_string
|
||||
./unittest_heap_string > baseline_heap_string.txt
|
||||
rm -f unittest_heap_string
|
||||
|
||||
gcc ../ini.c -DINI_CALL_HANDLER_ON_NEW_SECTION=1 unittest.c -o unittest_call_handler_on_new_section
|
||||
./unittest_call_handler_on_new_section > baseline_call_handler_on_new_section.txt
|
||||
rm -f unittest_call_handler_on_new_section
|
||||
|
||||
gcc ../ini.c -DINI_ALLOW_NO_VALUE=1 unittest.c -o unittest_allow_no_value
|
||||
./unittest_allow_no_value > baseline_allow_no_value.txt
|
||||
rm -f unittest_allow_no_value
|
|
@ -0,0 +1,43 @@
|
|||
/* inih -- unit tests for ini_parse_string() */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "../ini.h"
|
||||
|
||||
int User;
|
||||
char Prev_section[50];
|
||||
|
||||
int dumper(void* user, const char* section, const char* name,
|
||||
const char* value)
|
||||
{
|
||||
User = *((int*)user);
|
||||
if (strcmp(section, Prev_section)) {
|
||||
printf("... [%s]\n", section);
|
||||
strncpy(Prev_section, section, sizeof(Prev_section));
|
||||
Prev_section[sizeof(Prev_section) - 1] = '\0';
|
||||
}
|
||||
printf("... %s=%s;\n", name, value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void parse(const char* name, const char* string) {
|
||||
static int u = 100;
|
||||
int e;
|
||||
|
||||
*Prev_section = '\0';
|
||||
e = ini_parse_string(string, dumper, &u);
|
||||
printf("%s: e=%d user=%d\n", name, e, User);
|
||||
u++;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
parse("empty string", "");
|
||||
parse("basic", "[section]\nfoo = bar\nbazz = buzz quxx");
|
||||
parse("crlf", "[section]\r\nhello = world\r\nforty_two = 42\r\n");
|
||||
parse("long line", "[sec]\nfoo = 01234567890123456789\nbar=4321\n");
|
||||
parse("long continued", "[sec]\nfoo = 0123456789012bix=1234\n");
|
||||
parse("error", "[s]\na=1\nb\nc=3");
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
[section]
|
||||
a = b
|
||||
user = parse_error
|
||||
c = d
|
Loading…
Reference in New Issue