* Base configuration in /etc/repo_shell.cfg.
* Program name is now repo_shell
* Uses the inih library for ini parsing
-repo-shell
+repo_shell
*.swp
+.svn
--- /dev/null
+
+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.
--- /dev/null
+
+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/
--- /dev/null
+// Read an INI file into easy-to-access name/value pairs.
+
+#include <algorithm>
+#include <cctype>
+#include <cstdlib>
+#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;
+}
--- /dev/null
+// 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 <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.
+ 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<std::string, std::string> _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__
--- /dev/null
+// Example that shows simple usage of the INIReader class
+
+#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") << ", active="
+ << reader.GetBoolean("user", "active", true) << "\n";
+ return 0;
+}
--- /dev/null
+// CFG(section, name, default)
+
+CFG(protocol, version, "0")
+
+CFG(user, name, "Fatty Lumpkin")
+CFG(user, email, "fatty@lumpkin.com")
+
+#undef CFG
--- /dev/null
+/* 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;
+}
--- /dev/null
+/* 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;
+}
--- /dev/null
+/* 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;
+}
--- /dev/null
+; 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
--- /dev/null
+/* 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 <stdio.h>
+#include <ctype.h>
+#include <string.h>
+
+#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;
+}
--- /dev/null
+/* 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 <stdio.h>
+
+/* 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__ */
--- /dev/null
+This is an error
--- /dev/null
+[section1]
+name1=value1
+[section2
+[section3 ; comment ]
+name2=value2
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+[bom_section]
+bom_name=bom_value
\ No newline at end of file
--- /dev/null
+[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
--- /dev/null
+; 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
--- /dev/null
+@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
--- /dev/null
+/* 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 <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 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;
+}
--- /dev/null
+[section]
+a = b
+user = parse_error
+c = d
+++ /dev/null
-#include <stdio.h>
-#include <errno.h>
-#include <stdlib.h>
-#include <stdarg.h>
-#include <sys/types.h>
-#include <fcntl.h>
-#include <unistd.h>
-#include <pwd.h>
-#include <string.h>
-
-/* 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));
- }
-}
--- /dev/null
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <string.h>
+#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));
+ }
+}