1 /*** yuck-scmver.c -- snarf versions off project cwds
2  *
3  * Copyright (C) 2013-2016 Sebastian Freundt
4  *
5  * Author:  Sebastian Freundt <freundt@ga-group.nl>
6  *
7  * This file is part of yuck.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  *
13  * 1. Redistributions of source code must retain the above copyright
14  *    notice, this list of conditions and the following disclaimer.
15  *
16  * 2. Redistributions in binary form must reproduce the above copyright
17  *    notice, this list of conditions and the following disclaimer in the
18  *    documentation and/or other materials provided with the distribution.
19  *
20  * 3. Neither the name of the author nor the names of any contributors
21  *    may be used to endorse or promote products derived from this
22  *    software without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
28  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
31  * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
32  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
33  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
34  * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35  *
36  ***/
37 #if defined HAVE_CONFIG_H
38 # include "config.h"
39 #endif	/* HAVE_CONFIG_H */
40 #include <unistd.h>
41 #include <stdarg.h>
42 #include <stdlib.h>
43 #include <stdbool.h>
44 #include <stdio.h>
45 #include <string.h>
46 #include <assert.h>
47 #include <errno.h>
48 #include <fcntl.h>
49 #include <sys/wait.h>
50 #include <sys/stat.h>
51 #include "yuck-scmver.h"
52 
53 #if !defined LIKELY
54 # define LIKELY(_x)	__builtin_expect((_x), 1)
55 #endif	/* !LIKELY */
56 #if !defined UNLIKELY
57 # define UNLIKELY(_x)	__builtin_expect((_x), 0)
58 #endif	/* UNLIKELY */
59 #if !defined UNUSED
60 # define UNUSED(_x)	_x __attribute__((unused))
61 #endif	/* !UNUSED */
62 
63 #if !defined countof
64 # define countof(x)	(sizeof(x) / sizeof(*x))
65 #endif	/* !countof */
66 
67 #define _paste(x, y)	x ## y
68 #define paste(x, y)	_paste(x, y)
69 #if !defined with
70 # define with(args...)							\
71 	for (args, *paste(__ep, __LINE__) = (void*)1;			\
72 	     paste(__ep, __LINE__); paste(__ep, __LINE__) = 0)
73 #endif	/* !with */
74 
75 #define DEBUG(args...)
76 
77 /* globals */
78 const char *const yscm_strs[] = {
79 	[YUCK_SCM_TARBALL] = "tarball",
80 	[YUCK_SCM_GIT] = "git",
81 	[YUCK_SCM_BZR] = "bzr",
82 	[YUCK_SCM_HG] = "hg",
83 };
84 
85 
86 static __attribute__((format(printf, 1, 2))) void
error(const char * fmt,...)87 error(const char *fmt, ...)
88 {
89 	va_list vap;
90 	va_start(vap, fmt);
91 	vfprintf(stderr, fmt, vap);
92 	va_end(vap);
93 	if (errno) {
94 		fputc(':', stderr);
95 		fputc(' ', stderr);
96 		fputs(strerror(errno), stderr);
97 	}
98 	fputc('\n', stderr);
99 	return;
100 }
101 
102 static inline __attribute__((const, pure, always_inline)) char*
deconst(const char * s)103 deconst(const char *s)
104 {
105 	union {
106 		const char *c;
107 		char *p;
108 	} x = {s};
109 	return x.p;
110 }
111 
112 static __attribute__((unused)) size_t
xstrlcpy(char * restrict dst,const char * src,size_t dsz)113 xstrlcpy(char *restrict dst, const char *src, size_t dsz)
114 {
115 	size_t ssz;
116 
117 	if (UNLIKELY(dsz == 0U)) {
118 		return 0U;
119 	}
120 	if ((ssz = strlen(src)) > dsz) {
121 		ssz = dsz - 1U;
122 	}
123 	memcpy(dst, src, ssz);
124 	dst[ssz] = '\0';
125 	return ssz;
126 }
127 
128 static __attribute__((unused)) size_t
xstrlncpy(char * restrict dst,size_t dsz,const char * src,size_t ssz)129 xstrlncpy(char *restrict dst, size_t dsz, const char *src, size_t ssz)
130 {
131 	if (UNLIKELY(dsz == 0U)) {
132 		return 0U;
133 	}
134 	if (ssz > dsz) {
135 		ssz = dsz - 1U;
136 	}
137 	memcpy(dst, src, ssz);
138 	dst[ssz] = '\0';
139 	return ssz;
140 }
141 
142 static char*
xdirname(char * restrict fn,const char * fp)143 xdirname(char *restrict fn, const char *fp)
144 {
145 /* find next dir in FN from FP backwards */
146 	if (fp == NULL) {
147 		fp = fn + strlen(fn);
148 	} else if (fp <= fn) {
149 		return NULL;
150 	}
151 
152 	for (--fp; fp >= fn && *fp != '/'; fp--);
153 	while (fp >= fn && *--fp == '/');
154 	if (fp >= fn) {
155 		/* replace / by \nul and return pointer */
156 		char *dp = fn + (++fp - fn);
157 		*dp = '\0';
158 		return dp;
159 	}
160 	/* return \nul */
161 	return NULL;
162 }
163 
164 static char*
xmemmem(const char * hay,const size_t hayz,const char * ndl,const size_t ndlz)165 xmemmem(const char *hay, const size_t hayz, const char *ndl, const size_t ndlz)
166 {
167 	const char *const eoh = hay + hayz;
168 	const char *const eon = ndl + ndlz;
169 	const char *hp;
170 	const char *np;
171 	const char *cand;
172 	unsigned int hsum;
173 	unsigned int nsum;
174 	unsigned int eqp;
175 
176 	/* trivial checks first
177          * a 0-sized needle is defined to be found anywhere in haystack
178          * then run strchr() to find a candidate in HAYSTACK (i.e. a portion
179          * that happens to begin with *NEEDLE) */
180 	if (ndlz == 0UL) {
181 		return deconst(hay);
182 	} else if ((hay = memchr(hay, *ndl, hayz)) == NULL) {
183 		/* trivial */
184 		return NULL;
185 	}
186 
187 	/* First characters of haystack and needle are the same now. Both are
188 	 * guaranteed to be at least one character long.  Now computes the sum
189 	 * of characters values of needle together with the sum of the first
190 	 * needle_len characters of haystack. */
191 	for (hp = hay + 1U, np = ndl + 1U, hsum = *hay, nsum = *hay, eqp = 1U;
192 	     hp < eoh && np < eon;
193 	     hsum ^= *hp, nsum ^= *np, eqp &= *hp == *np, hp++, np++);
194 
195 	/* HP now references the (NZ + 1)-th character. */
196 	if (np < eon) {
197 		/* haystack is smaller than needle, :O */
198 		return NULL;
199 	} else if (eqp) {
200 		/* found a match */
201 		return deconst(hay);
202 	}
203 
204 	/* now loop through the rest of haystack,
205 	 * updating the sum iteratively */
206 	for (cand = hay; hp < eoh; hp++) {
207 		hsum ^= *cand++;
208 		hsum ^= *hp;
209 
210 		/* Since the sum of the characters is already known to be
211 		 * equal at that point, it is enough to check just NZ - 1
212 		 * characters for equality,
213 		 * also CAND is by design < HP, so no need for range checks */
214 		if (hsum == nsum && memcmp(cand, ndl, ndlz - 1U) == 0) {
215 			return deconst(cand);
216 		}
217 	}
218 	return NULL;
219 }
220 
221 static unsigned int
hextou(const char * sp,char ** ep)222 hextou(const char *sp, char **ep)
223 {
224 	register unsigned int res = 0U;
225 	size_t i;
226 
227 	if (UNLIKELY(sp == NULL)) {
228 		goto out;
229 	} else if (*sp == '\0') {
230 		goto out;
231 	}
232 	for (i = 0U; i < sizeof(res) * 8U / 4U - 1U; sp++, i++) {
233 		register unsigned int this;
234 		switch (*sp) {
235 		case '0' ... '9':
236 			this = *sp - '0';
237 			break;
238 		case 'a' ... 'f':
239 			this = *sp - 'a' + 10U;
240 			break;
241 		case 'A' ... 'F':
242 			this = *sp - 'A' + 10U;
243 			break;
244 		default:
245 			goto fucked;
246 		}
247 
248 		res <<= 4U;
249 		res |= this;
250 	}
251 fucked:
252 	res <<= 4U;
253 	res |= i;
254 
255 	/* keep reading the hexstring as long as it lasts */
256 	for (;; sp++) {
257 		switch (*sp) {
258 		case '0' ... '9':
259 		case 'a' ... 'f':
260 		case 'A' ... 'F':
261 			continue;
262 		default:
263 			goto out;
264 		}
265 	}
266 out:
267 	if (ep != NULL) {
268 		*ep = (char*)1U + (sp - (char*)1U);
269 	}
270 	return res;
271 }
272 
273 
274 /* version snarfers */
275 static __attribute__((noinline)) pid_t
run(int * fd,...)276 run(int *fd, ...)
277 {
278 	static char *cmdline[16U];
279 	va_list vap;
280 	pid_t p;
281 	/* to snarf off traffic from the child */
282 	int intfd[2];
283 
284 	va_start(vap, fd);
285 	for (size_t i = 0U;
286 	     i < countof(cmdline) &&
287 		     (cmdline[i] = va_arg(vap, char*)) != NULL; i++);
288 	va_end(vap);
289 	assert(*cmdline);
290 
291 	if (pipe(intfd) < 0) {
292 		error("pipe setup to/from %s failed", cmdline[0U]);
293 		return -1;
294 	}
295 
296 	switch ((p = vfork())) {
297 	case -1:
298 		/* i am an error */
299 		error("vfork for %s failed", cmdline[0U]);
300 		return -1;
301 
302 	default:
303 		/* i am the parent */
304 		close(intfd[1]);
305 		if (fd != NULL) {
306 			*fd = intfd[0];
307 		} else {
308 			close(intfd[0]);
309 		}
310 		return p;
311 
312 	case 0:
313 		/* i am the child */
314 		break;
315 	}
316 
317 	/* child code here */
318 	close(intfd[0]);
319 	dup2(intfd[1], STDOUT_FILENO);
320 
321 	execvp(cmdline[0U], cmdline);
322 	error("execvp(%s) failed", cmdline[0U]);
323 	_exit(EXIT_FAILURE);
324 }
325 
326 static int
fin(pid_t p)327 fin(pid_t p)
328 {
329 	int rc = 2;
330 	int st;
331 
332 	while (waitpid(p, &st, 0) != p);
333 	if (WIFEXITED(st)) {
334 		rc = WEXITSTATUS(st);
335 	}
336 	return rc;
337 }
338 
339 static yuck_scm_t
find_scm(char * restrict fn,size_t fz,const char * path)340 find_scm(char *restrict fn, size_t fz, const char *path)
341 {
342 	struct stat st[1U];
343 	char *restrict dp = fn;
344 
345 	/* make a copy so we can fiddle with it */
346 	if (UNLIKELY(path == NULL)) {
347 	cwd:
348 		/* just use "." then */
349 		*dp++ = '.';
350 		*dp = '\0';
351 	} else if ((dp += xstrlcpy(fn, path, fz)) == fn) {
352 		goto cwd;
353 	}
354 again:
355 	if (stat(fn, st) < 0) {
356 		return YUCK_SCM_ERROR;
357 	} else if (UNLIKELY((size_t)(dp - fn) + 5U >= fz)) {
358 		/* not enough space */
359 		return YUCK_SCM_ERROR;
360 	} else if (!S_ISDIR(st->st_mode)) {
361 		/* not a directory, get the dir bit and start over */
362 		if ((dp = xdirname(fn, dp)) == NULL) {
363 			dp = fn;
364 			goto cwd;
365 		}
366 		goto again;
367 	}
368 
369 scm_chk:
370 	/* now check for .git, .bzr, .hg */
371 	xstrlcpy(dp, "/.git", fz - (dp - fn));
372 	DEBUG("trying %s ...\n", fn);
373 	if (stat(fn, st) == 0 && S_ISDIR(st->st_mode)) {
374 		/* yay it's a .git */
375 		*dp = '\0';
376 		return YUCK_SCM_GIT;
377 	}
378 
379 	xstrlcpy(dp, "/.bzr", fz - (dp - fn));
380 	DEBUG("trying %s ...\n", fn);
381 	if (stat(fn, st) == 0 && S_ISDIR(st->st_mode)) {
382 		/* yay it's a .git */
383 		*dp = '\0';
384 		return YUCK_SCM_BZR;
385 	}
386 
387 	xstrlcpy(dp, "/.hg", fz - (dp - fn));
388 	DEBUG("trying %s ...\n", fn);
389 	if (stat(fn, st) == 0 && S_ISDIR(st->st_mode)) {
390 		/* yay it's a .git */
391 		*dp = '\0';
392 		return YUCK_SCM_HG;
393 	}
394 	/* nothing then, traverse upwards */
395 	if (*fn != '/') {
396 		/* make sure we don't go up indefinitely
397 		 * comparing the current inode to ./.. */
398 		with (ino_t curino) {
399 			*dp = '\0';
400 			if (stat(fn, st) < 0) {
401 				return YUCK_SCM_ERROR;
402 			}
403 			/* memorise inode */
404 			curino = st->st_ino;
405 			/* go upwards by appending /.. */
406 			dp += xstrlcpy(dp, "/..", fz - (dp - fn));
407 			/* check inode again */
408 			if (stat(fn, st) < 0) {
409 				return YUCK_SCM_ERROR;
410 			} else if (st->st_ino == curino) {
411 				break;
412 			}
413 			goto scm_chk;
414 		}
415 	} else if ((dp = xdirname(fn, dp)) != NULL) {
416 		goto scm_chk;
417 	}
418 	return YUCK_SCM_TARBALL;
419 }
420 
421 
422 static int
rd_version(struct yuck_version_s * restrict v,const char * buf,size_t bsz)423 rd_version(struct yuck_version_s *restrict v, const char *buf, size_t bsz)
424 {
425 /* reads a normalised version string vX.Y.Z-DIST-SCM RVSN[-dirty] */
426 	static const char dflag[] = "dirty";
427 	const char *vtag = NULL;
428 	const char *eov;
429 	const char *dist = NULL;
430 	const char *eod;
431 	const char *bp = buf;
432 	const char *const ep = buf + bsz;
433 
434 	/* parse buf */
435 	switch (*bp) {
436 	case 'v':
437 	case 'V':
438 		bp++;
439 	case '0':
440 	case '1':
441 	case '2':
442 	case '3':
443 	case '4':
444 	case '5':
445 	case '6':
446 	case '7':
447 	case '8':
448 	case '9':
449 		break;
450 	default:
451 		/* weird, we req'd v-tags */
452 		return -1;
453 	}
454 
455 	if ((eov = memchr(vtag = bp, '-', ep - bp)) == NULL) {
456 		/* last field */
457 		eov = ep;
458 	} else {
459 		dist = eov + 1U;
460 	}
461 	/* just for the fun of it, look for .git, .hg and .bzr as well */
462 	with (const char *altp) {
463 		if ((altp = xmemmem(vtag, ep - vtag, ".git", 4U))) {
464 			v->scm = YUCK_SCM_GIT;
465 			eov = altp;
466 			dist = altp + 4U;
467 		} else if ((altp = xmemmem(vtag, ep - vtag, ".bzr", 4U))) {
468 			/* oooh looks like the alternative version
469 			 * vX.Y.Z.gitDD.HASH */
470 			v->scm = YUCK_SCM_BZR;
471 			eov = altp;
472 			dist = altp + 4U;
473 		} else if ((altp = xmemmem(vtag, ep - vtag, ".hg", 3U))) {
474 			/* oooh looks like the alternative version
475 			 * vX.Y.Z.hgDD.HASH */
476 			v->scm = YUCK_SCM_HG;
477 			eov = altp;
478 			dist = altp + 3U;
479 		}
480 	}
481 
482 	/* bang vtag */
483 	xstrlncpy(v->vtag, sizeof(v->vtag), vtag, eov - vtag);
484 
485 	/* snarf distance */
486 	if (dist == NULL) {
487 		return 0;
488 	}
489 	/* read distance */
490 	with (char *on) {
491 		v->dist = strtoul(dist, &on, 10);
492 		eod = on;
493 	}
494 
495 	switch (*eod) {
496 	default:
497 	case '\0':
498 		return 0;
499 	case '.':
500 		if (v->scm <= YUCK_SCM_TARBALL) {
501 			/* huh? */
502 			return -1;
503 		}
504 		/*@fallthrough@*/
505 	case '-':
506 		/* the show is going on, like it must */
507 		bp = eod + 1U;
508 		break;
509 	}
510 	switch (*bp++) {
511 	case 'g':
512 		/* git repo */
513 		v->scm = YUCK_SCM_GIT;
514 		break;
515 	case 'h':
516 		/* hg repo */
517 		v->scm = YUCK_SCM_HG;
518 		break;
519 	case 'b':
520 		if (v->scm <= YUCK_SCM_TARBALL) {
521 			v->scm = YUCK_SCM_BZR;
522 			break;
523 		}
524 		/* else probably git or hg hash starting with b */
525 		/*@fallthrough@*/
526 	default:
527 		/* could have been set already then */
528 		if (v->scm > YUCK_SCM_TARBALL) {
529 			/* rewind bp and continue */
530 			bp--;
531 			break;
532 		}
533 		/* otherwise we simply don't know */
534 		return 0;
535 	}
536 	/* read scm revision */
537 	with (char *on) {
538 		v->rvsn = hextou(bp, &on);
539 		bp = on;
540 	}
541 
542 	if (bp >= ep) {
543 		;
544 	} else if (*bp != '-' && *bp != '.') {
545 		;
546 	} else if (bp + sizeof(dflag) > ep) {
547 		/* too short to fit `dirty' */
548 		;
549 	} else if (!memcmp(++bp, dflag, sizeof(dflag) - 1U)) {
550 		v->dirty = 1U;
551 	}
552 	return 0;
553 }
554 
555 static ssize_t
wr_version(char * restrict buf,size_t bsz,const struct yuck_version_s * v)556 wr_version(char *restrict buf, size_t bsz, const struct yuck_version_s *v)
557 {
558 	static const char yscm_abbr[] = "tgbh";
559 	const char *const ep = buf + bsz;
560 	char *bp = buf;
561 
562 	if (UNLIKELY(buf == NULL || bsz == 0U)) {
563 		return -1;
564 	}
565 	*bp++ = 'v';
566 	bp += xstrlcpy(bp, v->vtag, ep - bp);
567 	if (!v->dist) {
568 		goto out;
569 	} else if (bp + 1U >= ep) {
570 		/* not enough space */
571 		return -1;
572 	}
573 	/* get the dist bit on the wire */
574 	*bp++ = '-';
575 	bp += snprintf(bp, ep - bp, "%u", v->dist);
576 	if (!v->rvsn || v->scm <= YUCK_SCM_TARBALL) {
577 		goto out;
578 	} else if (bp + 2U + 8U >= ep) {
579 		/* not enough space */
580 		return -1;
581 	}
582 	*bp++ = '-';
583 	*bp++ = yscm_abbr[v->scm];
584 	bp += snprintf(bp, ep - bp, "%0*x",
585 		       (int)(v->rvsn & 0x07U), v->rvsn >> 4U);
586 	if (!v->dirty) {
587 		goto out;
588 	} else if (bp + 1U + 5U >= ep) {
589 		/* not enough space */
590 		return -1;
591 	}
592 	bp += xstrlcpy(bp, "-dirty", ep - bp);
593 out:
594 	return bp - buf;
595 }
596 
597 static int
git_version(struct yuck_version_s v[static1U])598 git_version(struct yuck_version_s v[static 1U])
599 {
600 	pid_t chld;
601 	int fd[1U];
602 	int rc = 0;
603 
604 	if ((chld = run(fd, "git", "describe",
605 			"--tags", "--match=v[0-9]*",
606 			"--abbrev=8", "--dirty", NULL)) < 0) {
607 		return -1;
608 	}
609 	/* shouldn't be heaps, so just use a single read */
610 	with (char buf[256U]) {
611 		const char *vtag;
612 		const char *dist;
613 		char *bp;
614 		ssize_t nrd;
615 
616 		if ((nrd = read(*fd, buf, sizeof(buf))) <= 0) {
617 			/* no version then aye */
618 			rc = -1;
619 			break;
620 		}
621 		buf[nrd - 1U/* for \n*/] = '\0';
622 		/* parse buf */
623 		bp = buf;
624 		if (*bp++ != 'v') {
625 			/* weird, we req'd v-tags though */
626 			rc = -1;
627 			break;
628 		} else if ((bp = strchr(vtag = bp, '-')) != NULL) {
629 			/* tokenise sting */
630 			*bp++ = '\0';
631 		}
632 		/* bang vtag */
633 		xstrlcpy(v->vtag, vtag, sizeof(v->vtag));
634 
635 		/* snarf distance */
636 		if (bp == NULL) {
637 			break;
638 		} else if ((bp = strchr(dist = bp, '-')) != NULL) {
639 			/* tokenize */
640 			*bp++ = '\0';
641 		}
642 		/* read distance */
643 		v->dist = strtoul(dist, &bp, 10);
644 
645 		if (*++bp == 'g') {
646 			bp++;
647 			/* read scm revision */
648 			v->rvsn = hextou(bp, &bp);
649 		}
650 		if (*bp == '\0') {
651 			break;
652 		} else if (*bp == '-') {
653 			bp++;
654 		}
655 		if (!strcmp(bp, "dirty")) {
656 			v->dirty = 1U;
657 		}
658 	}
659 	close(*fd);
660 	if (fin(chld) != 0) {
661 		rc = -1;
662 	}
663 	return rc;
664 }
665 
666 static int
hg_version(struct yuck_version_s v[static1U])667 hg_version(struct yuck_version_s v[static 1U])
668 {
669 	pid_t chld;
670 	int fd[1U];
671 	int rc = 0;
672 
673 	if ((chld = run(fd, "hg", "log",
674 			"--rev", ".",
675 			"--template",
676 			"{latesttag}\t{latesttagdistance}\t{node|short}\n",
677 			NULL)) < 0) {
678 		return -1;
679 	}
680 	/* shouldn't be heaps, so just use a single read */
681 	with (char buf[256U]) {
682 		const char *vtag;
683 		const char *dist;
684 		char *bp;
685 		ssize_t nrd;
686 
687 		if ((nrd = read(*fd, buf, sizeof(buf))) <= 0) {
688 			/* no version then aye */
689 			rc = -1;
690 			break;
691 		}
692 		buf[nrd - 1U/* for \n*/] = '\0';
693 		/* parse buf */
694 		bp = buf;
695 		if (*bp++ != 'v') {
696 			/* technically we could request the latest v-tag
697 			 * but i'm no hg buff so fuck it */
698 			rc = -1;
699 			break;
700 		} else if ((bp = strchr(vtag = bp, '\t')) != NULL) {
701 			/* tokenise */
702 			*bp++ = '\0';
703 		}
704 		/* bang vtag */
705 		xstrlcpy(v->vtag, vtag, sizeof(v->vtag));
706 
707 		if (UNLIKELY(bp == NULL)) {
708 			/* huh? */
709 			rc = -1;
710 			break;
711 		} else if ((bp = strchr(dist = bp, '\t')) != NULL) {
712 			/* tokenise */
713 			*bp++ = '\0';
714 		}
715 		/* bang distance */
716 		v->dist = strtoul(dist, NULL, 10);
717 
718 		/* bang revision */
719 		v->rvsn = hextou(bp, NULL);
720 	}
721 	close(*fd);
722 	if (fin(chld) != 0) {
723 		rc = -1;
724 	}
725 	return rc;
726 }
727 
728 static int
bzr_version(struct yuck_version_s v[static1U])729 bzr_version(struct yuck_version_s v[static 1U])
730 {
731 	pid_t chld;
732 	int fd[1U];
733 	int rc = 0;
734 
735 	/* first get current revision number */
736 	if ((chld = run(fd, "bzr", "revno", NULL)) < 0) {
737 		return -1;
738 	}
739 	/* shouldn't be heaps, so just use a single read */
740 	with (char buf[256U]) {
741 		ssize_t nrd;
742 
743 		if ((nrd = read(*fd, buf, sizeof(buf))) <= 0) {
744 			/* no version then aye */
745 			break;
746 		}
747 		with (char *on) {
748 			v->rvsn = strtoul(buf, &on, 10);
749 			if (LIKELY(on != NULL)) {
750 				v->rvsn <<= 4U;
751 				v->rvsn |= on - buf;
752 			}
753 		}
754 	}
755 	close(*fd);
756 	if (fin(chld) != 0) {
757 		return -1;
758 	}
759 
760 	if ((chld = run(fd, "bzr", "tags",
761 			"--sort=time", NULL)) < 0) {
762 		return -1;
763 	}
764 	/* could be a lot, we only need the last line though */
765 	with (char buf[4096U]) {
766 		const char *vtag;
767 		size_t bz;
768 		char *bp;
769 		ssize_t nrd;
770 
771 		bp = buf;
772 		bz = sizeof(buf);
773 		while ((nrd = read(*fd, bp, bz)) == (ssize_t)bz) {
774 			/* find last line */
775 			while (bz-- > 0 && buf[bz] != '\n');
776 			/* reassess bz */
777 			bz++;
778 			/* reassess bp */
779 			bp = buf + (sizeof(buf) - bz);
780 			if (LIKELY(bz < sizeof(buf))) {
781 				memmove(buf, buf + bz, sizeof(buf) - bz);
782 			}
783 		}
784 		if (nrd <= 0) {
785 			/* no version then aye */
786 			break;
787 		}
788 		bp[nrd - 1U/* for \n*/] = '\0';
789 		/* find last line */
790 		bp += nrd;
791 		while (--bp >= buf && *bp != '\n');
792 
793 		/* parse buf */
794 		if (*++bp != 'v') {
795 			/* we want v tags, we could go back and see if
796 			 * there are any */
797 			rc = -1;
798 			break;
799 		} else if ((bp = strchr(vtag = ++bp, ' ')) != NULL) {
800 			/* tokenise */
801 			*bp++ = '\0';
802 		}
803 		/* bang vtag */
804 		xstrlcpy(v->vtag, vtag, sizeof(v->vtag));
805 
806 		if (bp == NULL) {
807 			break;
808 		}
809 		/* read over all the whitespace to find the tag's revno */
810 		with (unsigned int rno = strtoul(bp, NULL, 10)) {
811 			v->dist = v->rvsn - rno;
812 		}
813 	}
814 	close(*fd);
815 	if (fin(chld) != 0) {
816 		rc = -1;
817 	}
818 	return rc;
819 }
820 
821 
822 /* public api */
823 #if !defined PATH_MAX
824 # define PATH_MAX	(256U)
825 #endif	/* !PATH_MAX */
826 
827 int
yuck_version(struct yuck_version_s * restrict v,const char * path)828 yuck_version(struct yuck_version_s *restrict v, const char *path)
829 {
830 	char cwd[PATH_MAX];
831 	char fn[PATH_MAX];
832 	int rc = -1;
833 
834 	/* initialise result structure */
835 	memset(v, 0, sizeof(*v));
836 
837 	if (getcwd(cwd, sizeof(cwd)) == NULL) {
838 		return -1;
839 	}
840 
841 	switch ((v->scm = find_scm(fn, sizeof(fn), path))) {
842 	case YUCK_SCM_ERROR:
843 	case YUCK_SCM_TARBALL:
844 	default:
845 		/* can't determine version numbers in tarball, can we? */
846 		return -1;
847 	case YUCK_SCM_GIT:
848 	case YUCK_SCM_BZR:
849 	case YUCK_SCM_HG:
850 		if (chdir(fn) < 0) {
851 			break;
852 		}
853 		switch (v->scm) {
854 		case YUCK_SCM_GIT:
855 			rc = git_version(v);
856 			break;
857 		case YUCK_SCM_BZR:
858 			rc = bzr_version(v);
859 			break;
860 		case YUCK_SCM_HG:
861 			rc = hg_version(v);
862 			break;
863 		default:
864 			break;
865 		}
866 		if (chdir(cwd) < 0) {
867 			/* oh big cluster fuck */
868 			rc = -1;
869 		}
870 		break;
871 	}
872 	return rc;
873 }
874 
875 int
yuck_version_read(struct yuck_version_s * restrict ref,const char * fn)876 yuck_version_read(struct yuck_version_s *restrict ref, const char *fn)
877 {
878 	int rc = 0;
879 	int fd;
880 
881 	/* initialise result structure */
882 	memset(ref, 0, sizeof(*ref));
883 
884 	if (fn[0U] == '-' && fn[1U] == '\0') {
885 		fd = STDIN_FILENO;
886 	} else if ((fd = open(fn, O_RDONLY)) < 0) {
887 		return -1;
888 	}
889 	/* otherwise read and parse the string */
890 	with (char buf[256U]) {
891 		ssize_t nrd;
892 		char *bp;
893 
894 		if ((nrd = read(fd, buf, sizeof(buf))) <= 0) {
895 			/* no version then aye */
896 			rc = -1;
897 			break;
898 		} else if ((bp = memchr(buf, '\n', nrd)) != NULL) {
899 			/* just go with the first line */
900 			*bp = '\0';
901 			nrd = bp - buf;
902 		} else if ((size_t)nrd < sizeof(buf)) {
903 			/* finalise with \nul */
904 			buf[nrd] = '\0';
905 		} else {
906 			/* finalise with \nul, cutting off the last byte */
907 			buf[--nrd] = '\0';
908 		}
909 		/* otherwise just read him */
910 		rc = rd_version(ref, buf, nrd);
911 	}
912 	close(fd);
913 	return rc;
914 }
915 
916 ssize_t
yuck_version_write_fd(int fd,const struct yuck_version_s * ref)917 yuck_version_write_fd(int fd, const struct yuck_version_s *ref)
918 {
919 	char buf[256U];
920 	ssize_t nwr;
921 
922 	if ((nwr = wr_version(buf, sizeof(buf), ref)) <= 0) {
923 		return -1;
924 	}
925 	/* otherwise write */
926 	buf[nwr++] = '\n';
927 	return write(fd, buf, nwr);
928 }
929 
930 int
yuck_version_write(const char * fn,const struct yuck_version_s * ref)931 yuck_version_write(const char *fn, const struct yuck_version_s *ref)
932 {
933 	int rc = 0;
934 	int fd;
935 
936 	if (fn[0U] == '-' && fn[1U] == '\0') {
937 		fd = STDOUT_FILENO;
938 	} else if ((fd = open(fn, O_RDWR | O_CREAT | O_TRUNC, 0666)) < 0) {
939 		return -1;
940 	}
941 	if (yuck_version_write_fd(fd, ref) < 0) {
942 		rc = -1;
943 	}
944 	close(fd);
945 	return rc;
946 }
947 
948 int
yuck_version_cmp(yuck_version_t v1,yuck_version_t v2)949 yuck_version_cmp(yuck_version_t v1, yuck_version_t v2)
950 {
951 	if (v1->dist == 0U && v2->dist == 0U) {
952 		/* must be a tag then, innit? */
953 		return memcmp(v1->vtag, v2->vtag, sizeof(v1->vtag));
954 	}
955 	/* just brute force the comparison, consider -dirty > -clean */
956 	return memcmp(v1, v2, sizeof(*v1));
957 }
958 
959 
960 #if defined BOOTSTRAP
961 int
main(int argc,char * argv[])962 main(int argc, char *argv[])
963 {
964 /* usage would be yuck-scmver SCMDIR [REFERENCE] */
965 	static struct yuck_version_s v[1U];
966 	int rc = 0;
967 
968 	/* prefer reference file */
969 	if (argc > 2 && (rc = yuck_version_read(v, argv[2U])) == 0) {
970 		/* just use this one */
971 		;
972 	} else {
973 		rc = yuck_version(v, argv[1U]);
974 	}
975 	if (rc == 0) {
976 		fputs("define(YUCK_SCMVER_VERSION, ", stdout);
977 		fputs(v->vtag, stdout);
978 		if (v->scm > YUCK_SCM_TARBALL && v->dist) {
979 			fputc('.', stdout);
980 			fputs(yscm_strs[v->scm], stdout);
981 			fprintf(stdout, "%u.%0*x",
982 				v->dist,
983 				(int)(v->rvsn & 0x07U), v->rvsn >> 4U);
984 		}
985 		if (v->dirty) {
986 			fputs(".dirty", stdout);
987 		}
988 		fputs(")\n", stdout);
989 	}
990 	return -rc;
991 }
992 #endif	/* BOOTSTRAP */
993 
994 
995 #if defined CONFIGURE
996 int
main(int argc,char * argv[])997 main(int argc, char *argv[])
998 {
999 /* usage would be yuck-scmver [REFERENCE] */
1000 	static struct yuck_version_s v[1U];
1001 	int rc = 0;
1002 
1003 	if (argc > 1) {
1004 		rc = yuck_version_read(v, argv[1U]);
1005 #if defined VERSION_FILE
1006 	} else if ((rc = yuck_version_read(v, VERSION_FILE)) == 0) {
1007 		;
1008 #endif	/* VERSION_FILE */
1009 	} else {
1010 		rc = yuck_version(v, NULL);
1011 	}
1012 	/* print if successful */
1013 	if (rc == 0) {
1014 		fputs(v->vtag, stdout);
1015 		if (v->scm > YUCK_SCM_TARBALL && v->dist) {
1016 			fputc('.', stdout);
1017 			fputs(yscm_strs[v->scm], stdout);
1018 			fprintf(stdout, "%u.%0*x",
1019 				v->dist,
1020 				(int)(v->rvsn & 0x07U), v->rvsn >> 4U);
1021 		}
1022 		if (v->dirty) {
1023 			fputs(".dirty", stdout);
1024 		}
1025 		fputc('\n', stdout);
1026 	}
1027 	return -rc;
1028 }
1029 #endif	/* CONFIGURE */
1030 
1031 /* yuck-scmver.c ends here */
1032