aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Borkmann <daniel@iogearbox.net>2019-09-17 12:43:21 +0200
committerDaniel Borkmann <daniel@iogearbox.net>2019-09-25 22:39:42 +0200
commit76ee0b229225193165ac2a6f49a9b9d0a714463f (patch)
tree893746923f9a508aa291f9a1e7ebdada6612d9ec
downloadl2md-76ee0b229225193165ac2a6f49a9b9d0a714463f.tar.gz
l2md: initial import of lore 2 maildir
There it is. See README for more details and setup. Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
-rw-r--r--COPYING347
-rw-r--r--Makefile14
-rw-r--r--README93
-rw-r--r--config.c214
-rw-r--r--env.c98
-rw-r--r--l2md.c50
-rw-r--r--l2md.h127
-rw-r--r--l2md.service13
-rw-r--r--l2mdconfig14
-rw-r--r--mail.c44
-rw-r--r--muttrc12
-rw-r--r--repo.c277
-rw-r--r--utils.c254
13 files changed, 1557 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..437f3d6
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,347 @@
+ Note that the only valid version of the GPL as far as this project is
+ concerned is _this_ particular version of the license (i.e. v2, not v2.2
+ or v3.x or whatever), unless explicitly otherwise stated.
+
+ Daniel Borkmann
+
+-----------------------------------------------------------------------------
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..19b0ce8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+CFLAGS = -O2 -Wall -Werror
+LDFLAGS = -lgit2
+
+l2md: l2md.o config.o env.o utils.o repo.o mail.o
+ $(CC) -o $@ $^ $(LDFLAGS)
+
+install:
+ cp l2md /usr/bin/l2md
+
+uninstall:
+ $(RM) /usr/bin/l2md
+
+clean:
+ $(RM) *.o l2md
diff --git a/README b/README
new file mode 100644
index 0000000..e8ac03c
--- /dev/null
+++ b/README
@@ -0,0 +1,93 @@
+l2md - lore2maildir
+-------------------
+
+Quick and dirty hack to import lore.kernel.org list archives via git,
+export them in maildir format and keep them periodically synced.
+
+It can then be used in whichever mail client that supports maildir
+format, for example, mutt.
+
+Essentially, it avoids the need to subscribe to any of the lore lists
+via mail since all messages are now imported through git transport.
+
+Together with a smtp client like msmtp (which you may need anyway for
+git-send-email), it allows to interact on the mailing lists the usual
+way.
+
+All pretty basic and hacky at this point, patches very welcome. Please
+send them to Daniel Borkmann <daniel@iogearbox.net>.
+
+Build
+-----
+
+Links to -lgit2 which is shipped by pretty much all major distros.
+
+Fedora: libgit2-devel
+Ubuntu: libgit2-dev
+
+To build, just type:
+
+$ make
+[...]
+
+After setting up ~/.l2mdconfig (see below), run as:
+
+$ ./l2md
+
+To install, just type:
+
+# make install
+
+The l2md.service file contains an example systemd service deployment
+for letting it run in the background ...
+
+$ service l2md status
+Redirecting to /bin/systemctl status l2md.service
+● l2md.service - lore2maildir
+ Loaded: loaded (/usr/lib/systemd/system/l2md.service; enabled; vendor preset: disabled)
+ Active: active (running) since Thu 2019-09-19 12:07:17 CEST; 55s ago
+ Main PID: 31467 (l2md)
+ Tasks: 1 (limit: 4915)
+ Memory: 257.9M
+ CGroup: /system.slice/l2md.service
+ └─31467 /usr/bin/l2md
+
+The muttrc file contains an example mutt config for importing the generated
+maildir directories.
+
+Howto
+-----
+
+The repo has a few example configs: l2mdconfig and muttrc. The latter
+needs most likey no further explanation, but just to provide an example
+on how to import the directories into mutt.
+
+The l2mdconfig is an example l2md config which needs to be placed under
+~/.l2mdconfig .
+
+$ cat ~/.l2mdconfig
+[general]
+ maildir = ~/.l2md/maildir/common
+ period = 30
+
+# bpf@vger.kernel.org list
+[repo bpf]
+ url = https://lore.kernel.org/bpf/0
+ maildir = ~/.l2md/maildir/bpf
+
+# netdev@vger.kernel.org list
+[repo netdev]
+ url = https://lore.kernel.org/netdev/1
+ url = https://lore.kernel.org/netdev/0
+ initial_import = 1000
+
+The general section contains a sync period in seconds where l2md refetches
+all the git repos and looks for new messages to export into the configured
+maildirs. The maildir under general is a path to a shared maildir where
+l2md exports new mails into. This can also be specified on a per repository
+basis.
+
+The repo sections with subsequent name define a repository (duh!) with
+one or more git urls to lore and optional maildir export path as mentioned.
+If initial_import is set to >0, then it will only import first x mails upon
+initial repository creation instead of the entire archive.
diff --git a/config.c b/config.c
new file mode 100644
index 0000000..eb6d356
--- /dev/null
+++ b/config.c
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <wordexp.h>
+#include <stdbool.h>
+
+#include "l2md.h"
+
+enum {
+ STATE_NONE = 0,
+ STATE_GENERAL,
+ STATE_REPO,
+};
+
+static void config_dump(struct config *cfg)
+{
+ struct config_repo *repo;
+ struct config_url *url;
+ uint32_t i, j;
+
+ if (!verbose_enabled)
+ return;
+
+ verbose("general.maildir = %s\n", cfg->general.maildir);
+ verbose("general.period = %u\n", cfg->general.period);
+
+ repo_for_each(cfg, repo, i) {
+ verbose("repos.%s.maildir = %s\n", repo->name, repo->maildir);
+ verbose("repos.%s.initial_import = %u\n", repo->name, repo->initial_import);
+ url_for_each(repo, url, j) {
+ verbose("repos.%s.url = %s\n", repo->name, url->path);
+ verbose("repos.%s.oid_maildir = %s\n",
+ repo->name, url->oid_known ? url->oid_maildir :
+ "[unknown]");
+ }
+ }
+}
+
+static void config_probe_oids(struct config *cfg)
+{
+ struct config_repo *repo;
+ struct config_url *url;
+ char path[PATH_MAX];
+ uint32_t i, j;
+ int ret;
+
+ repo_for_each(cfg, repo, i) {
+ url_for_each(repo, url, j) {
+ repo_local_oid(cfg, repo, url, path, sizeof(path));
+ ret = xread_file(path, url->oid_maildir,
+ sizeof(url->oid_maildir) - 1, false);
+ if (!ret)
+ url->oid_known = true;
+ }
+ }
+}
+
+static void config_set_maildir(struct config *cfg, const char *dir, bool root)
+{
+ struct config_repo *repo = repo_last(cfg);
+ char *maildir = root ? cfg->general.maildir : repo->maildir;
+ wordexp_t p;
+
+ wordexp(dir, &p, 0);
+ dir = p.we_wordv[0];
+ strlcpy(maildir, dir, sizeof(repo->maildir));
+ wordfree(&p);
+}
+
+static void config_set_initial_import(struct config *cfg, uint32_t limit)
+{
+ struct config_repo *repo = repo_last(cfg);
+
+ repo->initial_import = limit;
+ if (repo->initial_import > 0)
+ repo->limit = true;
+}
+
+static void config_new_url(struct config *cfg, const char *git_url)
+{
+ struct config_repo *repo = repo_last(cfg);
+ struct config_url *url;
+
+ repo->urls_num++;
+ repo->urls = xrealloc(repo->urls, sizeof(*repo->urls) *
+ repo->urls_num);
+ url = url_last(repo);
+ memset(url, 0, sizeof(*url));
+ strlcpy(url->path, git_url, sizeof(url->path));
+}
+
+static void config_new_repo(struct config *cfg, const char *name)
+{
+ struct config_repo *repo;
+
+ cfg->repos_num++;
+ cfg->repos = xrealloc(cfg->repos, sizeof(*cfg->repos) *
+ cfg->repos_num);
+ repo = repo_last(cfg);
+ memset(repo, 0, sizeof(*repo));
+ config_set_maildir(cfg, cfg->general.maildir, false);
+ strlcpy(repo->name, name, sizeof(repo->name));
+}
+
+static void config_set_defaults(struct config *cfg, const char *homedir)
+{
+ char path[PATH_MAX];
+
+ cfg->general.period = 60;
+
+ slprintf(path, sizeof(path), "%s/.l2md/", homedir);
+ strlcpy(cfg->general.base, path, sizeof(cfg->general.base));
+
+ slprintf(path, sizeof(path), "%s/.l2md/maildir", homedir);
+ strlcpy(cfg->general.maildir, path, sizeof(cfg->general.maildir));
+}
+
+void config_uninit(struct config *cfg)
+{
+ struct config_repo *repo;
+ uint32_t i;
+
+ repo_for_each(cfg, repo, i)
+ xfree(repo->urls);
+ xfree(cfg->repos);
+ xfree(cfg);
+}
+
+struct config *config_init(int argc, char **argv)
+{
+ const char *homedir = getenv("HOME");
+ char buff[1024], tmp[1024];
+ char path[PATH_MAX];
+ int state = STATE_NONE;
+ struct config *cfg;
+ FILE *fp;
+
+ if (argc > 1) {
+ if (argc == 2 && !strncmp(argv[1], "--verbose",
+ sizeof("--verbose")))
+ verbose_enabled = true;
+ else
+ panic("Usage: %s [--verbose]\n", argv[0]);
+ }
+
+ if (!homedir)
+ panic("Cannot retrieve $HOME from env!\n");
+
+ slprintf(path, sizeof(path), "%s/.l2mdconfig", homedir);
+ fp = fopen(path, "r");
+ if (!fp)
+ panic("Cannot open config %s: %s\n", tmp, strerror(errno));
+
+ cfg = xzmalloc(sizeof(*cfg));
+ config_set_defaults(cfg, homedir);
+
+ while (fgets(buff, sizeof(buff), fp)) {
+ uint32_t val;
+
+ if (buff[0] == '#' || buff[0] == '\n')
+ continue;
+
+ switch (state) {
+ case STATE_NONE:
+ state_next:
+ if (!strcmp(buff, "[general]\n")) {
+ state = STATE_GENERAL;
+ continue;
+ } else if (sscanf(buff, "[repo %[a-z0-9-]]\n",
+ tmp) == 1) {
+ state = STATE_REPO;
+ config_new_repo(cfg, tmp);
+ continue;
+ } else {
+ panic("Cannot parse: '%s'\n", buff);
+ }
+ break;
+
+ case STATE_GENERAL:
+ if (sscanf(buff, "\tperiod = %u", &val) == 1)
+ cfg->general.period = val;
+ else if (sscanf(buff, "\tmaildir = %s", tmp) == 1)
+ config_set_maildir(cfg, tmp, true);
+ else
+ goto state_next;
+ break;
+
+ case STATE_REPO:
+ if (sscanf(buff, "\turl = %s", tmp) == 1)
+ config_new_url(cfg, tmp);
+ else if (sscanf(buff, "\tmaildir = %s", tmp) == 1)
+ config_set_maildir(cfg, tmp, false);
+ else if (sscanf(buff, "\tinitial_import = %u", &val) == 1)
+ config_set_initial_import(cfg, val);
+ else
+ goto state_next;
+ break;
+
+ default:
+ panic("Invalid parser state: %d\n", state);
+ };
+ }
+
+ fclose(fp);
+
+ config_probe_oids(cfg);
+ config_dump(cfg);
+
+ return cfg;
+}
diff --git a/env.c b/env.c
new file mode 100644
index 0000000..3139fe2
--- /dev/null
+++ b/env.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
+
+#include <stdlib.h>
+#include <string.h>
+#include <libgen.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "l2md.h"
+
+static void bootstrap_base(struct config *cfg)
+{
+ xmkdir1(cfg->general.base);
+ xmkdir2(cfg->general.base, REPOS);
+ xmkdir2(cfg->general.base, OIDS);
+}
+
+static void bootstrap_repos(struct config *cfg)
+{
+ struct config_repo *repo;
+ struct config_url *url;
+ struct stat sb = {};
+ char path[PATH_MAX];
+ uint32_t i, j;
+ int ret;
+
+ verbose("Initial repository check.\n");
+
+ repo_for_each(cfg, repo, i) {
+ slprintf(path, sizeof(path), "%s/%s", REPOS, repo->name);
+ xmkdir2(cfg->general.base, path);
+
+ slprintf(path, sizeof(path), "%s/%s", OIDS, repo->name);
+ xmkdir2(cfg->general.base, path);
+
+ url_for_each(repo, url, j) {
+ repo_local_path(cfg, repo, url, path, sizeof(path));
+ ret = stat(path, &sb);
+ if (ret)
+ repo_clone(repo, url, path);
+ else
+ repo_pull(repo, url, path);
+ }
+ }
+
+ verbose("Initial repository check completed.\n");
+}
+
+static void bootstrap_mail(struct config *cfg)
+{
+ struct config_repo *repo;
+ uint32_t i;
+
+ repo_for_each(cfg, repo, i) {
+ xmkdir1_with_subdirs(repo->maildir);
+
+ xmkdir2(repo->maildir, "cur");
+ xmkdir2(repo->maildir, "tmp");
+ xmkdir2(repo->maildir, "new");
+ }
+}
+
+void bootstrap_env(struct config *cfg)
+{
+ bootstrap_base(cfg);
+ bootstrap_mail(cfg);
+ bootstrap_repos(cfg);
+
+ verbose("Bootstrap done.\n");
+}
+
+void sync_done(struct config *cfg)
+{
+ verbose("Sync done. Sleeping %us.\n", cfg->general.period);
+
+ sleep(cfg->general.period);
+}
+
+void sync_env(struct config *cfg)
+{
+ struct config_repo *repo;
+ struct config_url *url;
+ char path[PATH_MAX];
+ uint32_t i, j;
+
+ verbose("Resyncing repositories.\n");
+
+ repo_for_each(cfg, repo, i) {
+ url_for_each(repo, url, j) {
+ repo_local_path(cfg, repo, url, path, sizeof(path));
+ repo_pull(repo, url, path);
+ }
+ }
+}
diff --git a/l2md.c b/l2md.c
new file mode 100644
index 0000000..458cda0
--- /dev/null
+++ b/l2md.c
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <unistd.h>
+#include <git2.h>
+
+#include "l2md.h"
+
+pid_t own_pid;
+
+static volatile sig_atomic_t sigint;
+
+static void signal_handler(int number)
+{
+ if (number == SIGINT)
+ sigint = 1;
+}
+
+static __attribute__((constructor)) void main_init(void)
+{
+ git_libgit2_init();
+ own_pid = getpid();
+}
+
+static __attribute__((destructor)) void main_exit(void)
+{
+ git_libgit2_shutdown();
+}
+
+int main(int argc, char **argv)
+{
+ struct config *cfg = config_init(argc, argv);
+
+ bootstrap_env(cfg);
+ signal(SIGINT, signal_handler);
+
+ while (!sigint) {
+ sync_mail(cfg);
+ sync_done(cfg);
+ if (sigint)
+ break;
+ sync_env(cfg);
+ }
+
+ config_uninit(cfg);
+ return 0;
+}
diff --git a/l2md.h b/l2md.h
new file mode 100644
index 0000000..7b406a6
--- /dev/null
+++ b/l2md.h
@@ -0,0 +1,127 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
+
+#ifndef L2MD_H
+#define L2MD_H
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <git2.h>
+#include <unistd.h>
+
+#include <sys/types.h>
+#include <sys/time.h>
+
+/* libgit2 backwards compat crap. */
+#ifndef GIT_OBJECT_BLOB
+# define GIT_OLD_VERSION 1
+#endif
+#ifdef GIT_OLD_VERSION
+# define GIT_OBJECT_BLOB GIT_OBJ_BLOB
+# define GIT_OBJECT_COMMIT GIT_OBJ_COMMIT
+# define git_error_last giterr_last
+#endif
+
+#ifndef __check_format_printf
+# define __check_format_printf(pos_fmtstr, pos_fmtargs) \
+ __attribute__ ((format (printf, (pos_fmtstr), (pos_fmtargs))))
+#endif
+
+#define REPOS "repos"
+#define OIDS "oids"
+
+#define MAIL "m"
+
+struct config_general {
+ char maildir[PATH_MAX];
+ char base[PATH_MAX];
+ uint32_t period;
+};
+
+struct config_url {
+ char path[PATH_MAX];
+ char oid_maildir[GIT_OID_HEXSZ + 1];
+ bool oid_known;
+};
+
+struct config_repo {
+ char name[128];
+ char maildir[PATH_MAX];
+ git_repository *git;
+ struct config_url *urls;
+ uint32_t urls_num;
+ uint32_t initial_import;
+ bool limit;
+};
+
+struct config {
+ struct config_general general;
+ struct config_repo *repos;
+ uint32_t repos_num;
+};
+
+#define repo_for_each(cfg, repo, i) \
+ for (repo = &cfg->repos[(i = 0)]; i < cfg->repos_num; \
+ repo = &cfg->repos[++i])
+
+#define url_for_each(repo, url, i) \
+ for (url = &repo->urls[(i = 0)]; i < repo->urls_num; \
+ url = &repo->urls[++i])
+
+#define repo_last(cfg) \
+ &cfg->repos[cfg->repos_num - 1]
+
+#define url_last(repo) \
+ &repo->urls[repo->urls_num - 1]
+
+extern pid_t own_pid;
+extern bool verbose_enabled;
+
+struct config *config_init(int argc, char **argv);
+void config_uninit(struct config *cfg);
+
+void bootstrap_env(struct config *cfg);
+
+void sync_env(struct config *cfg);
+void sync_mail(struct config *cfg);
+void sync_done(struct config *cfg);
+
+void repo_clone(struct config_repo *repo, struct config_url *url, const char *target);
+void repo_pull(struct config_repo *repo, struct config_url *url, const char *target);
+void repo_walk_files(struct config *cfg, struct config_repo *repo, uint32_t which,
+ const char *path, const char *oid_last, char *oid_done,
+ void (*repo_walker)(struct config *cfg, struct config_repo *repo,
+ uint32_t which, const char *oid,
+ const void *raw, size_t len));
+
+void repo_local_path(struct config *cfg, struct config_repo *repo,
+ struct config_url *url, char *out, size_t len);
+void repo_local_oid(struct config *cfg, struct config_repo *repo,
+ struct config_url *url, char *out, size_t len);
+
+void *xmalloc(size_t size);
+void *xzmalloc(size_t size);
+void *xrealloc(void *old, size_t size);
+void xfree(void *ptr);
+
+void xmkdir1(const char *path);
+void xmkdir2(const char *base, const char *name);
+void xmkdir1_with_subdirs(const char *path);
+
+int xread_file(const char *file, char *to, size_t len, bool fatal);
+int xwrite_file(const char *file, const char *to, size_t len, bool fatal);
+
+int timeval_sub(struct timeval *res, struct timeval *x,
+ struct timeval *y);
+
+size_t strlcpy(char *dest, const char *src, size_t size);
+int slprintf(char *dst, size_t size, const char *fmt, ...);
+
+void verbose(const char *format, ...) __check_format_printf(1, 2);
+void panic(const char *format, ...) __check_format_printf(1, 2);
+void warn(const char *format, ...) __check_format_printf(1, 2);
+void die(void);
+
+#endif /* L2MD_H */
diff --git a/l2md.service b/l2md.service
new file mode 100644
index 0000000..917cac2
--- /dev/null
+++ b/l2md.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=lore2maildir
+After=network-online.target
+
+[Service]
+Type=simple
+User=darkstar
+WorkingDirectory=/home/darkstar/
+ExecStart=/usr/bin/l2md
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/l2mdconfig b/l2mdconfig
new file mode 100644
index 0000000..8222324
--- /dev/null
+++ b/l2mdconfig
@@ -0,0 +1,14 @@
+[general]
+ maildir = ~/.l2md/maildir/common
+ period = 30
+
+# bpf@vger.kernel.org list
+[repo bpf]
+ url = https://lore.kernel.org/bpf/0
+ maildir = ~/.l2md/maildir/bpf
+
+# netdev@vger.kernel.org list
+[repo netdev]
+ url = https://lore.kernel.org/netdev/1
+ url = https://lore.kernel.org/netdev/0
+ initial_import = 1000
diff --git a/mail.c b/mail.c
new file mode 100644
index 0000000..238da96
--- /dev/null
+++ b/mail.c
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
+
+#include <unistd.h>
+
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include "l2md.h"
+
+static void repo_walker(struct config *cfg, struct config_repo *repo, uint32_t which,
+ const char *oid, const void *raw, size_t len)
+{
+ char dst[PATH_MAX];
+
+ slprintf(dst, sizeof(dst), "%s/new/0.%06u.%s-%u-%s",
+ repo->maildir, own_pid, repo->name, which, oid);
+
+ xwrite_file(dst, raw, len, true);
+}
+
+void sync_mail(struct config *cfg)
+{
+ struct config_repo *repo;
+ struct config_url *url;
+ char path[PATH_MAX];
+ uint32_t i, j;
+
+ verbose("Resyncing maildirs.\n");
+
+ repo_for_each(cfg, repo, i) {
+ url_for_each(repo, url, j) {
+ repo_local_path(cfg, repo, url, path, sizeof(path));
+ repo_walk_files(cfg, repo, j, path,
+ url->oid_known ? url->oid_maildir : NULL,
+ url->oid_maildir, repo_walker);
+
+ repo_local_oid(cfg, repo, url, path, sizeof(path));
+ xwrite_file(path, url->oid_maildir,
+ sizeof(url->oid_maildir) - 1, true);
+ url->oid_known = true;
+ }
+ }
+}
diff --git a/muttrc b/muttrc
new file mode 100644
index 0000000..d4a1910
--- /dev/null
+++ b/muttrc
@@ -0,0 +1,12 @@
+# Setting up maildir and pointing l2md bpf dir to it:
+set mbox_type = Maildir
+set folder = "~/.l2md/maildir/bpf/"
+set header_cache = "~/.cache/mutt"
+set spoolfile = +/
+
+# Sorting by discussion threads:
+set sort = "threads"
+set strict_threads = "yes"
+set sort_browser = "reverse-date"
+set sort_aux = "reverse-last-date-received"
+unset collapse_unread
diff --git a/repo.c b/repo.c
new file mode 100644
index 0000000..e4c6ec5
--- /dev/null
+++ b/repo.c
@@ -0,0 +1,277 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
+
+#include <stdio.h>
+#include <string.h>
+#include <libgen.h>
+
+#include "l2md.h"
+
+static int progress_sideband(const char *msg, int len, void *private_data)
+{
+ verbose("Remote: %.*s", len, msg);
+ fflush(stdout);
+ return 0;
+}
+
+static void progress_checkout(const char *path, size_t curr, size_t total,
+ void *private_data)
+{
+ verbose("Checkout: %zu/%zu\r", curr, total);
+ fflush(stdout);
+}
+
+static void setup_fetch_opts(git_fetch_options *opts)
+{
+ opts->callbacks.sideband_progress = progress_sideband;
+}
+
+static void setup_checkout_opts(git_checkout_options *opts)
+{
+ opts->checkout_strategy = GIT_CHECKOUT_SAFE;
+ opts->progress_cb = progress_checkout;
+}
+
+static void setup_clone_opts(git_clone_options *opts)
+{
+ setup_checkout_opts(&opts->checkout_opts);
+ setup_fetch_opts(&opts->fetch_opts);
+}
+
+static void panic_git(const char *subject)
+{
+ const git_error *err = git_error_last();
+ const char *err_str = err ? err->message : "unknown error";
+
+ panic("%s: %s\n", subject, err_str);
+}
+
+static void warn_git(const char *subject)
+{
+ const git_error *err = git_error_last();
+ const char *err_str = err ? err->message : "unknown error";
+
+ warn("%s: %s\n", subject, err_str);
+}
+
+void repo_clone(struct config_repo *repo, struct config_url *url,
+ const char *target)
+{
+ git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT;
+ int ret;
+
+ setup_clone_opts(&clone_opts);
+
+ verbose("Cloning: %s\n", url->path);
+
+ ret = git_clone(&repo->git, url->path, target, &clone_opts);
+ if (ret)
+ panic_git("Cannot clone git repo");
+
+ git_repository_free(repo->git);
+}
+
+static int fetch_head(const char *ref_name, const char *remote_url,
+ const git_oid *oid, unsigned int merge,
+ void *private_data)
+{
+ char out[GIT_OID_HEXSZ + 1];
+
+ if (merge) {
+ verbose("Merging: %s commit %s\n", ref_name,
+ git_oid_tostr(out, sizeof(out), oid));
+ git_oid_cpy(private_data, oid);
+ }
+
+ return 0;
+}
+
+void repo_pull(struct config_repo *repo, struct config_url *url,
+ const char *target)
+{
+ git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+ git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
+ git_reference *head_cur, *head_new;
+ git_annotated_commit *head;
+ git_oid oid_to_merge;
+ git_object *head_obj;
+ git_remote *remote;
+ int ret;
+
+ ret = git_repository_open(&repo->git, target);
+ if (ret)
+ panic_git("Cannot open git repo");
+
+ ret = git_remote_lookup(&remote, repo->git, "origin");
+ if (ret)
+ panic_git("Cannot look up origin");
+
+ setup_fetch_opts(&fetch_opts);
+
+ verbose("Fetching: %s\n", url->path);
+
+ ret = git_remote_fetch(remote, NULL, &fetch_opts,
+ NULL);
+ if (ret) {
+ warn_git("Cannot fetch remote");
+ goto out;
+ }
+
+ git_repository_fetchhead_foreach(repo->git, fetch_head,
+ &oid_to_merge);
+
+ ret = git_annotated_commit_lookup(&head, repo->git,
+ &oid_to_merge);
+ if (ret)
+ panic_git("Cannot lookup head");
+
+ ret = git_repository_head(&head_cur, repo->git);
+ if (ret)
+ panic_git("Cannot lookup current head");
+
+ ret = git_object_lookup(&head_obj, repo->git, &oid_to_merge,
+ GIT_OBJECT_COMMIT);
+ if (ret)
+ panic_git("Cannot lookup HEAD object");
+
+ setup_checkout_opts(&checkout_opts);
+
+ ret = git_checkout_tree(repo->git, head_obj, &checkout_opts);
+ if (ret)
+ panic_git("Cannot checkout tree");
+
+ ret = git_reference_set_target(&head_new, head_cur, &oid_to_merge,
+ NULL);
+ if (ret)
+ panic_git("Cannot repoint HEAD");
+
+ git_repository_state_cleanup(repo->git);
+
+ git_annotated_commit_free(head);
+
+ git_reference_free(head_cur);
+ git_reference_free(head_new);
+
+ git_object_free(head_obj);
+out:
+ git_remote_free(remote);
+ git_repository_free(repo->git);
+}
+
+static void __repo_local_get(struct config *cfg, struct config_repo *repo,
+ struct config_url *url, const char *which,
+ char *out, size_t len)
+{
+ char tmp[PATH_MAX];
+
+ strcpy(tmp, url->path);
+ slprintf(out, len, "%s/%s/%s/%s", cfg->general.base, which,
+ repo->name, basename(tmp));
+}
+
+void repo_local_path(struct config *cfg, struct config_repo *repo,
+ struct config_url *url, char *out, size_t len)
+{
+ __repo_local_get(cfg, repo, url, REPOS, out, len);
+}
+
+void repo_local_oid(struct config *cfg, struct config_repo *repo,
+ struct config_url *url, char *out, size_t len)
+{
+ __repo_local_get(cfg, repo, url, OIDS, out, len);
+}
+
+void repo_walk_files(struct config *cfg, struct config_repo *repo, uint32_t which,
+ const char *path, const char *oid_last, char *oid_done,
+ void (*repo_walker)(struct config *cfg, struct config_repo *repo,
+ uint32_t which, const char *oid,
+ const void *raw, size_t len))
+{
+ char oid_curr[GIT_OID_HEXSZ + 1];
+ struct timeval start, end, res;
+ git_oid coid, loid, foid;
+ bool have_first = false;
+ const git_blob *blob;
+ char spec[PATH_MAX];
+ git_revwalk *walker;
+ git_commit *commit;
+ git_object *object;
+ uint32_t count = 0;
+ int ret;
+
+ ret = git_repository_open(&repo->git, path);
+ if (ret)
+ panic_git("Cannot open git repo");
+
+ ret = git_revwalk_new(&walker, repo->git);
+ if (ret)
+ panic_git("Cannot create ref walker");
+
+ git_revwalk_sorting(walker, GIT_SORT_TIME);
+
+ ret = git_revwalk_push_head(walker);
+ if (ret)
+ panic_git("Cannot push head onto ref walker");
+ if (oid_last) {
+ ret = git_oid_fromstrn(&loid, oid_last, GIT_OID_HEXSZ);
+ if (ret)
+ panic_git("Cannot convert to git oid");
+ }
+
+ gettimeofday(&start, NULL);
+
+ while (!git_revwalk_next(&coid, walker)) {
+ ret = git_commit_lookup(&commit, repo->git, &coid);
+ if (ret)
+ panic_git("Cannot look up commit");
+
+ if (!have_first) {
+ git_oid_cpy(&foid, &coid);
+ have_first = true;
+ }
+
+ if (oid_last && !git_oid_cmp(&loid, &coid)) {
+ git_commit_free(commit);
+ break;
+ }
+
+ if (!oid_last && repo->limit) {
+ if (repo->initial_import == 0) {
+ git_commit_free(commit);
+ break;
+ }
+ repo->initial_import--;
+ }
+
+ git_oid_tostr(oid_curr, sizeof(oid_curr), &coid);
+ slprintf(spec, sizeof(spec), "%s:%s", oid_curr, MAIL);
+
+ ret = git_revparse_single(&object, repo->git, spec);
+ if (ret || git_object_type(object) != GIT_OBJECT_BLOB)
+ panic_git("Cannot revparse object");
+
+ blob = (const git_blob *)object;
+ repo_walker(cfg, repo, which, oid_curr, git_blob_rawcontent(blob),
+ (size_t)git_blob_rawsize(blob));
+
+ git_object_free(object);
+ git_commit_free(commit);
+
+ count++;
+ }
+
+ gettimeofday(&end, NULL);
+ timeval_sub(&res, &end, &start);
+
+ if (have_first)
+ git_oid_tostr(oid_done, GIT_OID_HEXSZ + 1, &foid);
+ if (have_first && count) {
+ git_oid_tostr(oid_curr, sizeof(oid_curr), &loid);
+ printf("Processed %u new mail(s) for %s. %s: %s -> %s in %ld.%02lds.\n",
+ count, repo->name, repo->urls[which].path, oid_last ? oid_curr :
+ "[none]", oid_done, res.tv_sec, res.tv_usec);
+ }
+
+ git_revwalk_free(walker);
+ git_repository_free(repo->git);
+}
diff --git a/utils.c b/utils.c
new file mode 100644
index 0000000..d97af0e
--- /dev/null
+++ b/utils.c
@@ -0,0 +1,254 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/syscall.h>
+
+#include "l2md.h"
+
+bool verbose_enabled;
+
+void *xmalloc(size_t size)
+{
+ void *ptr;
+
+ if (!size)
+ panic("xmalloc: zero size\n");
+ ptr = malloc(size);
+ if (ptr == NULL)
+ panic("xmalloc: out of memory (allocating %zu bytes)\n", size);
+ return ptr;
+}
+
+void *xzmalloc(size_t size)
+{
+ void *ptr = xmalloc(size);
+
+ memset(ptr, 0, size);
+ return ptr;
+}
+
+void *xrealloc(void *old, size_t size)
+{
+ void *new;
+
+ if (size == 0)
+ panic("xrealloc: zero size\n");
+ new = realloc(old, size);
+ if (new == NULL)
+ panic("xrealloc: out of memory (allocating %zu bytes)\n", size);
+ return new;
+}
+
+void xfree(void *ptr)
+{
+ free(ptr);
+}
+
+void xmkdir1(const char *path)
+{
+ struct stat sb = {};
+ int ret;
+
+ ret = stat(path, &sb);
+ if (ret) {
+ ret = mkdir(path, S_IRWXU);
+ if (ret)
+ panic("mkdir %s failed: %s\n", path, strerror(errno));
+ }
+}
+
+void xmkdir1_with_subdirs(const char *path)
+{
+ char tmp[PATH_MAX], *ptr;
+ size_t len;
+
+ slprintf(tmp, sizeof(tmp),"%s", path);
+ len = strlen(tmp);
+ if (tmp[len - 1] == '/')
+ tmp[len - 1] = 0;
+ for (ptr = tmp + 1; *ptr; ptr++) {
+ if (*ptr == '/') {
+ *ptr = 0;
+ xmkdir1(tmp);
+ *ptr = '/';
+ }
+ }
+ xmkdir1(tmp);
+}
+
+void xmkdir2(const char *base, const char *name)
+{
+ char path[PATH_MAX];
+
+ slprintf(path, sizeof(path), "%s/%s", base, name);
+ xmkdir1(path);
+}
+
+int xread_file(const char *file, char *to, size_t len, bool fatal)
+{
+ loff_t ret;
+ int fd;
+
+ fd = open(file, O_RDONLY);
+ if (fd < 0) {
+ if (fatal)
+ panic("Cannot open %s: %s", file, strerror(errno));
+ else
+ return fd;
+ }
+
+ do {
+ ret = read(fd, to, len);
+ if (ret < 0) {
+ if (fatal) {
+ panic("Cannot read file %s: %s", file,
+ strerror(errno));
+ } else {
+ close(fd);
+ return -1;
+ }
+ }
+ to += ret;
+ len -= ret;
+ } while (len > 0);
+
+ close(fd);
+ return 0;
+}
+
+int xwrite_file(const char *file, const char *to, size_t len, bool fatal)
+{
+ loff_t ret;
+ int fd;
+
+ fd = open(file, O_CREAT | O_WRONLY | O_TRUNC, 0600);
+ if (fd < 0) {
+ if (fatal)
+ panic("Cannot open %s: %s", file, strerror(errno));
+ else
+ return fd;
+ }
+
+ do {
+ ret = write(fd, to, len);
+ if (ret < 0) {
+ if (fatal) {
+ panic("Cannot write file %s: %s", file,
+ strerror(errno));
+ } else {
+ close(fd);
+ return -1;
+ }
+ }
+ to += ret;
+ len -= ret;
+ } while (len > 0);
+
+ close(fd);
+ return 0;
+}
+
+int timeval_sub(struct timeval *res, struct timeval *x,
+ struct timeval *y)
+{
+ if (x->tv_usec < y->tv_usec) {
+ int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1;
+
+ y->tv_usec -= 1000000 * nsec;
+ y->tv_sec += nsec;
+ }
+
+ if (x->tv_usec - y->tv_usec > 1000000) {
+ int nsec = (x->tv_usec - y->tv_usec) / 1000000;
+
+ y->tv_usec += 1000000 * nsec;
+ y->tv_sec -= nsec;
+ }
+
+ res->tv_sec = x->tv_sec - y->tv_sec;
+ res->tv_usec = x->tv_usec - y->tv_usec;
+
+ return x->tv_sec < y->tv_sec;
+}
+
+size_t strlcpy(char *dest, const char *src, size_t size)
+{
+ size_t ret = strlen(src);
+
+ if (size) {
+ size_t len = (ret >= size) ? size - 1 : ret;
+
+ memcpy(dest, src, len);
+ dest[len] = '\0';
+ }
+
+ return ret;
+}
+
+static inline int vslprintf(char *dst, size_t size, const char *fmt, va_list ap)
+{
+ int ret;
+
+ ret = vsnprintf(dst, size, fmt, ap);
+ dst[size - 1] = '\0';
+
+ return ret;
+}
+
+int slprintf(char *dst, size_t size, const char *fmt, ...)
+{
+ va_list ap;
+ int ret;
+
+ va_start(ap, fmt);
+ ret = vslprintf(dst, size, fmt, ap);
+ va_end(ap);
+
+ return ret;
+}
+
+void die(void)
+{
+ exit(EXIT_FAILURE);
+}
+
+void panic(const char *format, ...)
+{
+ va_list vl;
+
+ va_start(vl, format);
+ vfprintf(stderr, format, vl);
+ va_end(vl);
+
+ die();
+}
+
+void warn(const char *format, ...)
+{
+ va_list vl;
+
+ va_start(vl, format);
+ vfprintf(stderr, format, vl);
+ va_end(vl);
+}
+
+void verbose(const char *format, ...)
+{
+ va_list vl;
+
+ if (verbose_enabled) {
+ va_start(vl, format);
+ vfprintf(stdout, format, vl);
+ va_end(vl);
+ }
+}