From: R. Steve McKown Date: Sun, 23 Sep 2012 18:49:37 +0000 (-0600) Subject: Add support for /etc/repo_shell.cfg X-Git-Tag: 0.1~3 X-Git-Url: https://oss.titaniummirror.com/gitweb?p=repo_shell.git;a=commitdiff_plain;h=bd8087cf1e4569fdbd4f09fa2bbb01c4c072e007 Add support for /etc/repo_shell.cfg * Base configuration in /etc/repo_shell.cfg. * Program name is now repo_shell * Uses the inih library for ini parsing --- diff --git a/.gitignore b/.gitignore index b5dba6c..baba52a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -repo-shell +repo_shell *.swp +.svn diff --git a/inih/LICENSE.txt b/inih/LICENSE.txt new file mode 100644 index 0000000..1d31de2 --- /dev/null +++ b/inih/LICENSE.txt @@ -0,0 +1,27 @@ + +The "inih" library is distributed under the New BSD license: + +Copyright (c) 2009, Brush Technology +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 Brush Technology 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 BRUSH TECHNOLOGY ''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 BRUSH TECHNOLOGY 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. diff --git a/inih/README.txt b/inih/README.txt new file mode 100644 index 0000000..8e5f8b1 --- /dev/null +++ b/inih/README.txt @@ -0,0 +1,5 @@ + +inih is a simple .INI file parser written in C, released under the New BSD +license (see LICENSE.txt). Go to the project home page for more info: + +http://code.google.com/p/inih/ diff --git a/inih/cpp/INIReader.cpp b/inih/cpp/INIReader.cpp new file mode 100644 index 0000000..62ae74f --- /dev/null +++ b/inih/cpp/INIReader.cpp @@ -0,0 +1,67 @@ +// Read an INI file into easy-to-access name/value pairs. + +#include +#include +#include +#include "../ini.h" +#include "INIReader.h" + +using std::string; + +INIReader::INIReader(string filename) +{ + _error = ini_parse(filename.c_str(), ValueHandler, this); +} + +int INIReader::ParseError() +{ + return _error; +} + +string INIReader::Get(string section, string name, string default_value) +{ + string key = MakeKey(section, name); + return _values.count(key) ? _values[key] : default_value; +} + +long INIReader::GetInteger(string section, string name, long default_value) +{ + 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; +} + +bool INIReader::GetBoolean(string section, string name, bool default_value) +{ + 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; +} + +string INIReader::MakeKey(string section, 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 = (INIReader*)user; + string key = MakeKey(section, name); + if (reader->_values[key].size() > 0) + reader->_values[key] += "\n"; + reader->_values[key] += value; + return 1; +} diff --git a/inih/cpp/INIReader.h b/inih/cpp/INIReader.h new file mode 100644 index 0000000..b519a22 --- /dev/null +++ b/inih/cpp/INIReader.h @@ -0,0 +1,48 @@ +// Read an INI file into easy-to-access name/value pairs. + +// inih and INIReader are released under the New BSD license (see LICENSE.txt). +// Go to the project home page for more info: +// +// http://code.google.com/p/inih/ + +#ifndef __INIREADER_H__ +#define __INIREADER_H__ + +#include +#include + +// 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. + INIReader(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(); + + // Get a string value from INI file, returning default_value if not found. + std::string Get(std::string section, std::string name, + std::string default_value); + + // 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(std::string section, std::string name, long default_value); + + // 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(std::string section, std::string name, bool default_value); + +private: + int _error; + std::map _values; + static std::string MakeKey(std::string section, std::string name); + static int ValueHandler(void* user, const char* section, const char* name, + const char* value); +}; + +#endif // __INIREADER_H__ diff --git a/inih/cpp/INIReaderTest.cpp b/inih/cpp/INIReaderTest.cpp new file mode 100644 index 0000000..abbf219 --- /dev/null +++ b/inih/cpp/INIReaderTest.cpp @@ -0,0 +1,20 @@ +// Example that shows simple usage of the INIReader class + +#include +#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") << ", active=" + << reader.GetBoolean("user", "active", true) << "\n"; + return 0; +} diff --git a/inih/examples/config.def b/inih/examples/config.def new file mode 100644 index 0000000..6113252 --- /dev/null +++ b/inih/examples/config.def @@ -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 diff --git a/inih/examples/ini_dump.c b/inih/examples/ini_dump.c new file mode 100644 index 0000000..87253ee --- /dev/null +++ b/inih/examples/ini_dump.c @@ -0,0 +1,40 @@ +/* ini.h example that simply dumps an INI file without comments */ + +#include +#include +#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; +} diff --git a/inih/examples/ini_example.c b/inih/examples/ini_example.c new file mode 100644 index 0000000..0973572 --- /dev/null +++ b/inih/examples/ini_example.c @@ -0,0 +1,44 @@ +/* Example: parse a simple configuration file */ + +#include +#include +#include +#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; +} diff --git a/inih/examples/ini_xmacros.c b/inih/examples/ini_xmacros.c new file mode 100644 index 0000000..a2cab43 --- /dev/null +++ b/inih/examples/ini_xmacros.c @@ -0,0 +1,46 @@ +/* Parse a configuration file into a struct using X-Macros */ + +#include +#include +#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; +} diff --git a/inih/examples/test.ini b/inih/examples/test.ini new file mode 100644 index 0000000..be56ae4 --- /dev/null +++ b/inih/examples/test.ini @@ -0,0 +1,9 @@ +; 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 diff --git a/inih/ini.c b/inih/ini.c new file mode 100644 index 0000000..15546e9 --- /dev/null +++ b/inih/ini.c @@ -0,0 +1,158 @@ +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +http://code.google.com/p/inih/ + +*/ + +#include +#include +#include + +#include "ini.h" + +#define MAX_LINE 200 +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* 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(*--p)) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +static char* lskip(const char* s) +{ + while (*s && isspace(*s)) + s++; + return (char*)s; +} + +/* Return pointer to first char c or ';' comment in given string, or pointer to + null at end of string if neither found. ';' must be prefixed by a whitespace + character to register as a comment. */ +static char* find_char_or_comment(const char* s, char c) +{ + int was_whitespace = 0; + while (*s && *s != c && !(was_whitespace && *s == ';')) { + was_whitespace = isspace(*s); + s++; + } + 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); + dest[size - 1] = '\0'; + return dest; +} + +/* See documentation in header file. */ +int ini_parse_file(FILE* file, + int (*handler)(void*, const char*, const char*, + const char*), + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ + char line[MAX_LINE]; + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + + /* Scan through file line by line */ + while (fgets(line, sizeof(line), file) != NULL) { + 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 (*start == ';' || *start == '#') { + /* Per Python ConfigParser, allow '#' comments at start of line */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { + /* Non-black 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_char_or_comment(start + 1, ']'); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start && *start != ';') { + /* Not a comment, must be a name[=:]value pair */ + end = find_char_or_comment(start, '='); + if (*end != '=') { + end = find_char_or_comment(start, ':'); + } + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = lskip(end + 1); + end = find_char_or_comment(value, '\0'); + if (*end == ';') + *end = '\0'; + 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 */ + error = lineno; + } + } + } + + return error; +} + +/* See documentation in header file. */ +int ini_parse(const char* filename, + int (*handler)(void*, const char*, const char*, const char*), + 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; +} diff --git a/inih/ini.h b/inih/ini.h new file mode 100644 index 0000000..f337a6a --- /dev/null +++ b/inih/ini.h @@ -0,0 +1,61 @@ +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +http://code.google.com/p/inih/ + +*/ + +#ifndef __INI_H__ +#define __INI_H__ + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* 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), or -1 on file open error. +*/ +int ini_parse(const char* filename, + int (*handler)(void* user, const char* section, + const char* name, const char* value), + 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, + int (*handler)(void* user, const char* section, + const char* name, const char* value), + 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 http://code.google.com/p/inih/issues/detail?id=21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __INI_H__ */ diff --git a/inih/tests/bad_comment.ini b/inih/tests/bad_comment.ini new file mode 100644 index 0000000..d4bab4a --- /dev/null +++ b/inih/tests/bad_comment.ini @@ -0,0 +1 @@ +This is an error diff --git a/inih/tests/bad_multi.ini b/inih/tests/bad_multi.ini new file mode 100644 index 0000000..3ec342f --- /dev/null +++ b/inih/tests/bad_multi.ini @@ -0,0 +1 @@ + indented diff --git a/inih/tests/bad_section.ini b/inih/tests/bad_section.ini new file mode 100644 index 0000000..689a4e5 --- /dev/null +++ b/inih/tests/bad_section.ini @@ -0,0 +1,5 @@ +[section1] +name1=value1 +[section2 +[section3 ; comment ] +name2=value2 diff --git a/inih/tests/baseline_multi.txt b/inih/tests/baseline_multi.txt new file mode 100644 index 0000000..2f4e21e --- /dev/null +++ b/inih/tests/baseline_multi.txt @@ -0,0 +1,46 @@ +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; +... [colon_tests] +... Content-Type=text/html; +... foo=bar; +... adams=42; +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; +bom.ini: e=0 user=107 diff --git a/inih/tests/baseline_single.txt b/inih/tests/baseline_single.txt new file mode 100644 index 0000000..a847fe9 --- /dev/null +++ b/inih/tests/baseline_single.txt @@ -0,0 +1,42 @@ +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; +... [colon_tests] +... Content-Type=text/html; +... foo=bar; +... adams=42; +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; +bom.ini: e=0 user=107 diff --git a/inih/tests/bom.ini b/inih/tests/bom.ini new file mode 100644 index 0000000..079d09b --- /dev/null +++ b/inih/tests/bom.ini @@ -0,0 +1,2 @@ +[bom_section] +bom_name=bom_value \ No newline at end of file diff --git a/inih/tests/multi_line.ini b/inih/tests/multi_line.ini new file mode 100644 index 0000000..b00f086 --- /dev/null +++ b/inih/tests/multi_line.ini @@ -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 diff --git a/inih/tests/normal.ini b/inih/tests/normal.ini new file mode 100644 index 0000000..bd5bcd7 --- /dev/null +++ b/inih/tests/normal.ini @@ -0,0 +1,25 @@ +; 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 + +[colon_tests] +Content-Type: text/html +foo:bar +adams : 42 diff --git a/inih/tests/unittest.bat b/inih/tests/unittest.bat new file mode 100644 index 0000000..d0e84cf --- /dev/null +++ b/inih/tests/unittest.bat @@ -0,0 +1,2 @@ +@call tcc ..\ini.c -I..\ -run unittest.c > baseline_multi.txt +@call tcc ..\ini.c -I..\ -DINI_ALLOW_MULTILINE=0 -run unittest.c > baseline_single.txt diff --git a/inih/tests/unittest.c b/inih/tests/unittest.c new file mode 100644 index 0000000..97643ca --- /dev/null +++ b/inih/tests/unittest.c @@ -0,0 +1,58 @@ +/* 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. + +Here's how I produced the two baseline files (with Tiny C Compiler): + +tcc -DINI_ALLOW_MULTILINE=1 ../ini.c -run unittest.c > baseline_multi.txt +tcc -DINI_ALLOW_MULTILINE=0 ../ini.c -run unittest.c > baseline_single.txt + +*/ + +#include +#include +#include +#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 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, (void*)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"); + return 0; +} diff --git a/inih/tests/user_error.ini b/inih/tests/user_error.ini new file mode 100644 index 0000000..6596387 --- /dev/null +++ b/inih/tests/user_error.ini @@ -0,0 +1,4 @@ +[section] +a = b +user = parse_error +c = d diff --git a/repo-shell.c b/repo-shell.c deleted file mode 100644 index 8399925..0000000 --- a/repo-shell.c +++ /dev/null @@ -1,332 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* TODO: these should come from a config file */ -static char *svn_repo_root = "/var/lib/svn/"; -static char *git_repo_root = "/var/lib/svn/"; -//static char *repo_user = "repo"; -static char *repo_user = "smckown"; - -#define alloc_nr(x) (((x)+16)*3/2) - -/* - * Realloc the buffer pointed at by variable 'x' so that it can hold - * at least 'nr' entries; the number of entries currently allocated - * is 'alloc', using the standard growing factor alloc_nr() macro. - * - * DO NOT USE any expression with side-effect for 'x', 'nr', or 'alloc'. - */ -#define ALLOC_GROW(x, nr, alloc) \ - do { \ - if ((nr) > alloc) { \ - if (alloc_nr(alloc) < (nr)) \ - alloc = (nr); \ - else \ - alloc = alloc_nr(alloc); \ - x = xrealloc((x), alloc * sizeof(*(x))); \ - } \ - } while (0) - -static inline void die(const char *fmt, ...) -{ - va_list ap; - - va_start(ap, fmt); - fprintf(stderr, "error: "); - vfprintf(stderr, fmt, ap); - fprintf(stderr, "\n" ); - va_end(ap); - exit(1); -} - -char *xstrdup(const char *str) -{ - char *ret = strdup(str); - if (!ret) - die("out of memory"); - return ret; -} - -void *xmalloc(size_t size) -{ - void *ret; - - ret = malloc(size); - if (!ret && !size) - ret = malloc(1); - if (!ret) - die("out of memory"); - return ret; -} - -void *xrealloc(void *ptr, size_t size) -{ - void *ret; - - ret = realloc(ptr, size); - if (!ret && !size) - ret = realloc(ptr, 1); - if (!ret) - die("Out of memory, realloc failed"); - return ret; -} - -static uid_t user_uid(char *user) -{ - struct passwd *pw = getpwnam(user); - - if (!pw) - die("invalid user %s", user); - return pw->pw_uid; -} - -static void change_user(char *user) -{ - /* This is the function for which setuid is required, as root */ - setuid(user_uid(user)); -} - -static char *dequote(char *arg) -{ - char* narg = NULL; - - if (arg && *arg == '\'') { - char* end = arg + strlen(arg) - 1; - - if (end != arg && *end == '\'') { - narg = arg + 1; - *end = '\0'; - } - } - return narg; -} - -static char *add_prefix(char *prefix, char* arg) -{ - int size; - - if (arg && prefix && strlen(prefix)) { - char *n = xmalloc(sizeof(char *) * - (strlen(prefix) + strlen(arg) + 2)); - strcpy(n, prefix); - strcat(n, "/"); - strcat(n, arg); - arg = n; - } - return arg; -} - -static int check_ssh_interactive(uid_t uid) -{ - /* TODO: Check the config file for the user owning uid to see if that - * user should be able to execute any commands other than those required - * to support repository access. Return a boolean true/false. - */ - return 1; /* for now */ -} - -static int git_check_access(const char *cmd, const char *arg, const char *user) -{ - /* TODO: Read some configuration file which maps users and access - * to a boolean true/false value. - * - * The git command can support read and write. - * git-receive-pack is ok for readers and writers - * git-upload-pack is ok only for writers - * git-upload-archive is ok only for writers - */ - return 1; /* assume OK for now */ -} - -static int do_git_cmd(const char *cmd, char *arg, char *user) -{ - const char *nargv[4]; - char* narg; - int ret; - - if (!(arg = dequote(arg))) - die("bad argument"); - if (strncmp(cmd, "git-", 4)) - die("bad command"); - - change_user(repo_user); - if (!git_check_access(cmd, arg, user)) - die("permission denied"); - - nargv[0] = cmd; - nargv[1] = add_prefix(git_repo_root, arg); - nargv[2] = NULL; - - ret = execvp(nargv[0], (char *const *) nargv); - /* Code unreached if execv successful */ - free(narg); - return ret; -} - -static int do_svnserve_cmd(const char *cmd, char *arg, char *user) -{ - const char *svnserve_argv[7] = { - cmd, "-t", "--root", svn_repo_root, "--tunnel-user", user, NULL - }; - int ret; - - change_user(repo_user); - return execvp(svnserve_argv[0], (char *const *) svnserve_argv); -} - -#define SPLIT_CMDLINE_BAD_ENDING 1 -#define SPLIT_CMDLINE_UNCLOSED_QUOTE 2 -static const char *split_cmdline_errors[] = { - "cmdline ends with \\", - "unclosed quote" -}; - -int split_cmdline(char *cmdline, const char ***argv) -{ - int src, dst, count = 0, size = 16; - char quoted = 0; - - *argv = xmalloc(sizeof(char *) * size); - - /* split alias_string */ - (*argv)[count++] = cmdline; - for (src = dst = 0; cmdline[src];) { - char c = cmdline[src]; - if (!quoted && isspace(c)) { - cmdline[dst++] = 0; - while (cmdline[++src] - && isspace(cmdline[src])) - ; /* skip */ - ALLOC_GROW(*argv, count+1, size); - (*argv)[count++] = cmdline + dst; - } else if (!quoted && (c == '\'' || c == '"')) { - quoted = c; - src++; - } else if (c == quoted) { - quoted = 0; - src++; - } else { - if (c == '\\' && quoted != '\'') { - src++; - c = cmdline[src]; - if (!c) { - free(*argv); - *argv = NULL; - return -SPLIT_CMDLINE_BAD_ENDING; - } - } - cmdline[dst++] = c; - src++; - } - } - - cmdline[dst] = 0; - - if (quoted) { - free(*argv); - *argv = NULL; - return -SPLIT_CMDLINE_UNCLOSED_QUOTE; - } - - ALLOC_GROW(*argv, count+1, size); - (*argv)[count] = NULL; - - return count; -} - -const char *split_cmdline_strerror(int split_cmdline_errno) { - return split_cmdline_errors[-split_cmdline_errno-1]; -} - -static void cd_to_homedir(void) -{ - const char *home = getenv("HOME"); - if (!home) - die("could not determine user's home directory; HOME is unset"); - if (chdir(home) == -1) - die("could not chdir to user's home directory"); -} - -static struct commands { - const char *name; - int (*exec)(const char *cmd, char *arg, char *user); -} cmd_list[] = { - { "git-receive-pack", do_git_cmd }, - { "git-upload-pack", do_git_cmd }, - { "git-upload-archive", do_git_cmd }, - { "svnserve", do_svnserve_cmd }, - { NULL }, -}; - -int main(int argc, char **argv) -{ - char *prog; - const char **user_argv; - struct commands *cmd; - int devnull_fd; - int count; - - /* - * Always open file descriptors 0/1/2 to avoid clobbering files - * in die(). It also avoids not messing up when the pipes are - * dup'ed onto stdin/stdout/stderr in the child processes we spawn. - */ - devnull_fd = open("/dev/null", O_RDWR); - while (devnull_fd >= 0 && devnull_fd <= 2) - devnull_fd = dup(devnull_fd); - if (devnull_fd == -1) - die("opening /dev/null failed"); - close (devnull_fd); - - if (argc < 3) - die("invalid arguments"); - fprintf(stderr, "prog |%s|\n", argv[2]); - prog = xstrdup(argv[2]); - if (!strncmp(prog, "git", 3) && isspace(prog[3])) - /* Accept "git foo" as if the caller said "git-foo". */ - prog[3] = '-'; - - for (cmd = cmd_list ; cmd->name ; cmd++) { - int len = strlen(cmd->name); - char *arg; - struct passwd *pw; - if (strncmp(cmd->name, prog, len)) - continue; - arg = NULL; - switch (prog[len]) { - case '\0': - arg = NULL; - break; - case ' ': - arg = prog + len + 1; - break; - default: - continue; - } - - pw = getpwuid(getuid()); - exit(cmd->exec(cmd->name, arg, pw->pw_name)); - } - - if (!check_ssh_interactive(getuid())) - die("only repository access is allowed"); - - cd_to_homedir(); - count = split_cmdline(prog, &user_argv); - if (count >= 0) { - execvp(user_argv[0], (char *const *) user_argv); - free(user_argv); - die("unrecognized command '%s'", argv[2]); - } else { - free(prog); - die("invalid command format '%s': %s", argv[2], - split_cmdline_strerror(count)); - } -} diff --git a/repo_shell.c b/repo_shell.c new file mode 100644 index 0000000..35edbe7 --- /dev/null +++ b/repo_shell.c @@ -0,0 +1,370 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ini.h" + +#define CFG_FILE "/etc/repo_shell.cfg" + +typedef struct { + char *svn_root; + char *git_root; + char *owner; +} cfg_t; + +#undef USE_DEFAULTS +#ifdef USE_DEFAULTS /* perhaps we want defaults? Not sure */ +static cfg_t cfg { + svn_root: "/var/lib/svn/repositories", + git_root: "/var/lib/git", + owner: "repo" +}; +#else +static cfg_t cfg; +#endif + +#define alloc_nr(x) (((x)+16)*3/2) + +/* + * Realloc the buffer pointed at by variable 'x' so that it can hold + * at least 'nr' entries; the number of entries currently allocated + * is 'alloc', using the standard growing factor alloc_nr() macro. + * + * DO NOT USE any expression with side-effect for 'x', 'nr', or 'alloc'. + */ +#define ALLOC_GROW(x, nr, alloc) \ + do { \ + if ((nr) > alloc) { \ + if (alloc_nr(alloc) < (nr)) \ + alloc = (nr); \ + else \ + alloc = alloc_nr(alloc); \ + x = xrealloc((x), alloc * sizeof(*(x))); \ + } \ + } while (0) + +static inline void die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + fprintf(stderr, "error: "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n" ); + va_end(ap); + exit(1); +} + +char *xstrdup(const char *str) +{ + char *ret = strdup(str); + if (!ret) + die("out of memory"); + return ret; +} + +void *xmalloc(size_t size) +{ + void *ret; + + ret = malloc(size); + if (!ret && !size) + ret = malloc(1); + if (!ret) + die("out of memory"); + return ret; +} + +void *xrealloc(void *ptr, size_t size) +{ + void *ret; + + ret = realloc(ptr, size); + if (!ret && !size) + ret = realloc(ptr, 1); + if (!ret) + die("Out of memory, realloc failed"); + return ret; +} + +static uid_t user_uid(char *user) +{ + struct passwd *pw = getpwnam(user); + + if (!pw) + die("invalid user %s", user); + return pw->pw_uid; +} + +static void change_user(char *user) +{ + /* This is the function for which setuid is required, as root */ + setuid(user_uid(user)); +} + +static char *dequote(char *arg) +{ + char* narg = NULL; + + if (arg && *arg == '\'') { + char* end = arg + strlen(arg) - 1; + + if (end != arg && *end == '\'') { + narg = arg + 1; + *end = '\0'; + } + } + return narg; +} + +static char *add_prefix(char *prefix, char* arg) +{ + int size; + + if (arg && prefix && strlen(prefix)) { + char *n = xmalloc(sizeof(char *) * + (strlen(prefix) + strlen(arg) + 2)); + strcpy(n, prefix); + strcat(n, "/"); + strcat(n, arg); + arg = n; + } + return arg; +} + +static int check_ssh_interactive(uid_t uid) +{ + /* TODO: Check the config file for the user owning uid to see if that + * user should be able to execute any commands other than those required + * to support repository access. Return a boolean true/false. + */ + return 1; /* for now */ +} + +static int git_check_access(const char *cmd, const char *arg, const char *user) +{ + /* TODO: Read some configuration file which maps users and access + * to a boolean true/false value. + * + * The git command can support read and write. + * git-receive-pack is ok for readers and writers + * git-upload-pack is ok only for writers + * git-upload-archive is ok only for writers + */ + return 1; /* assume OK for now */ +} + +static int do_git_cmd(const char *cmd, char *arg, char *user) +{ + const char *nargv[4]; + char* narg; + int ret; + + if (!(arg = dequote(arg))) + die("bad argument"); + if (strncmp(cmd, "git-", 4)) + die("bad command"); + + change_user(cfg.owner); + if (!git_check_access(cmd, arg, user)) + die("permission denied"); + + nargv[0] = cmd; + nargv[1] = add_prefix(cfg.git_root, arg); + nargv[2] = NULL; + + ret = execvp(nargv[0], (char *const *) nargv); + /* Code unreached if execv successful */ + free(narg); + return ret; +} + +static int do_svnserve_cmd(const char *cmd, char *arg, char *user) +{ + const char *svnserve_argv[7] = { + cmd, "-t", "--root", cfg.svn_root, "--tunnel-user", user, NULL + }; + int ret; + + change_user(cfg.owner); + return execvp(svnserve_argv[0], (char *const *) svnserve_argv); +} + +#define SPLIT_CMDLINE_BAD_ENDING 1 +#define SPLIT_CMDLINE_UNCLOSED_QUOTE 2 +static const char *split_cmdline_errors[] = { + "cmdline ends with \\", + "unclosed quote" +}; + +int split_cmdline(char *cmdline, const char ***argv) +{ + int src, dst, count = 0, size = 16; + char quoted = 0; + + *argv = xmalloc(sizeof(char *) * size); + + /* split alias_string */ + (*argv)[count++] = cmdline; + for (src = dst = 0; cmdline[src];) { + char c = cmdline[src]; + if (!quoted && isspace(c)) { + cmdline[dst++] = 0; + while (cmdline[++src] + && isspace(cmdline[src])) + ; /* skip */ + ALLOC_GROW(*argv, count+1, size); + (*argv)[count++] = cmdline + dst; + } else if (!quoted && (c == '\'' || c == '"')) { + quoted = c; + src++; + } else if (c == quoted) { + quoted = 0; + src++; + } else { + if (c == '\\' && quoted != '\'') { + src++; + c = cmdline[src]; + if (!c) { + free(*argv); + *argv = NULL; + return -SPLIT_CMDLINE_BAD_ENDING; + } + } + cmdline[dst++] = c; + src++; + } + } + + cmdline[dst] = 0; + + if (quoted) { + free(*argv); + *argv = NULL; + return -SPLIT_CMDLINE_UNCLOSED_QUOTE; + } + + ALLOC_GROW(*argv, count+1, size); + (*argv)[count] = NULL; + + return count; +} + +const char *split_cmdline_strerror(int split_cmdline_errno) { + return split_cmdline_errors[-split_cmdline_errno-1]; +} + +static void cd_to_homedir(void) +{ + const char *home = getenv("HOME"); + if (!home) + die("could not determine user's home directory; HOME is unset"); + if (chdir(home) == -1) + die("could not chdir to user's home directory"); +} + +static struct commands { + const char *name; + int (*exec)(const char *cmd, char *arg, char *user); +} cmd_list[] = { + { "git-receive-pack", do_git_cmd }, + { "git-upload-pack", do_git_cmd }, + { "git-upload-archive", do_git_cmd }, + { "svnserve", do_svnserve_cmd }, + { NULL }, +}; + +static int handler(void* user, const char* section, const char* name, + const char* value) +{ + cfg_t* pconfig = (cfg_t*)user; + + #define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0 + if (MATCH("core", "svn_root")) + pconfig->svn_root = xstrdup(value); + else if (MATCH("core", "git_root")) + pconfig->git_root = xstrdup(value); + else if (MATCH("core", "owner")) + pconfig->owner = xstrdup(value); + else + return 0; /* unknown section/name, error */ + return 1; +} + +int main(int argc, char **argv) +{ + char *prog; + const char **user_argv; + struct commands *cmd; + int devnull_fd; + int count; + + /* + * Always open file descriptors 0/1/2 to avoid clobbering files + * in die(). It also avoids not messing up when the pipes are + * dup'ed onto stdin/stdout/stderr in the child processes we spawn. + */ + devnull_fd = open("/dev/null", O_RDWR); + while (devnull_fd >= 0 && devnull_fd <= 2) + devnull_fd = dup(devnull_fd); + if (devnull_fd == -1) + die("opening /dev/null failed"); + close (devnull_fd); + + if (argc < 3) + die("invalid arguments"); + +#ifdef USE_DEFAULTS + ini_parse("repo_shell.cfg", handler, &cfg); +#else + if (ini_parse(CFG_FILE, handler, &cfg) < 0) + die("cannot read config file %s", CFG_FILE); +#endif + + prog = xstrdup(argv[2]); + if (!strncmp(prog, "git", 3) && isspace(prog[3])) + /* Accept "git foo" as if the caller said "git-foo". */ + prog[3] = '-'; + + for (cmd = cmd_list ; cmd->name ; cmd++) { + int len = strlen(cmd->name); + char *arg; + struct passwd *pw; + if (strncmp(cmd->name, prog, len)) + continue; + arg = NULL; + switch (prog[len]) { + case '\0': + arg = NULL; + break; + case ' ': + arg = prog + len + 1; + break; + default: + continue; + } + + pw = getpwuid(getuid()); + exit(cmd->exec(cmd->name, arg, pw->pw_name)); + } + + if (!check_ssh_interactive(getuid())) + die("only repository access is allowed"); + + cd_to_homedir(); + count = split_cmdline(prog, &user_argv); + if (count >= 0) { + execvp(user_argv[0], (char *const *) user_argv); + free(user_argv); + die("unrecognized command '%s'", argv[2]); + } else { + free(prog); + die("invalid command format '%s': %s", argv[2], + split_cmdline_strerror(count)); + } +}