diff options
| author | David Leonard <d+busybox@adaptive-enterprises.com> | 2022-02-20 14:29:45 +1000 |
|---|---|---|
| committer | Denys Vlasenko <vda.linux@googlemail.com> | 2022-05-02 14:25:36 +0200 |
| commit | 4642cf5b388bf60f6bea67ce3a5031d24bccd48a (patch) | |
| tree | 8f6cec270d99a47627b16402531db303076a34ef | |
| parent | 52a7bf6fa677abdb80f8e484f6ba77ed3d34e444 (diff) | |
| download | busybox-w32-4642cf5b388bf60f6bea67ce3a5031d24bccd48a.tar.gz busybox-w32-4642cf5b388bf60f6bea67ce3a5031d24bccd48a.tar.bz2 busybox-w32-4642cf5b388bf60f6bea67ce3a5031d24bccd48a.zip | |
tsort: new applet
function old new delta
tsort_main - 578 +578
.rodata 104884 104906 +22
applet_names 2759 2765 +6
applet_main 1596 1600 +4
packed_usage 34290 34288 -2
------------------------------------------------------------------------------
(add/remove: 2/0 grow/shrink: 3/1 up/down: 610/-2) Total: 608 bytes
Signed-off-by: David Leonard <d+busybox@adaptive-enterprises.com>
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
| -rw-r--r-- | coreutils/tsort.c | 188 | ||||
| -rw-r--r-- | docs/posix_conformance.txt | 2 | ||||
| -rwxr-xr-x | testsuite/tsort.tests | 110 |
3 files changed, 299 insertions, 1 deletions
diff --git a/coreutils/tsort.c b/coreutils/tsort.c new file mode 100644 index 000000000..dedb65b15 --- /dev/null +++ b/coreutils/tsort.c | |||
| @@ -0,0 +1,188 @@ | |||
| 1 | /* vi: set sw=4 ts=4: */ | ||
| 2 | /* | ||
| 3 | * tsort implementation for busybox | ||
| 4 | * | ||
| 5 | * public domain -- David Leonard, 2022 | ||
| 6 | */ | ||
| 7 | //config:config TSORT | ||
| 8 | //config: bool "tsort (0.7 kb)" | ||
| 9 | //config: default y | ||
| 10 | //config: help | ||
| 11 | //config: tsort performs a topological sort. | ||
| 12 | |||
| 13 | //applet:IF_TSORT(APPLET(tsort, BB_DIR_USR_BIN, BB_SUID_DROP)) | ||
| 14 | |||
| 15 | //kbuild:lib-$(CONFIG_TSORT) += tsort.o | ||
| 16 | |||
| 17 | /* BB_AUDIT SUSv3 compliant */ | ||
| 18 | /* http://www.opengroup.org/onlinepubs/007904975/utilities/tsort.html */ | ||
| 19 | |||
| 20 | //usage:#define tsort_trivial_usage | ||
| 21 | //usage: "[FILE]" | ||
| 22 | //usage:#define tsort_full_usage "\n\n" | ||
| 23 | //usage: "Topological sort" | ||
| 24 | //usage:#define tsort_example_usage | ||
| 25 | //usage: "$ echo -e \"a b\\nb c\" | tsort\n" | ||
| 26 | //usage: "a\n" | ||
| 27 | //usage: "b\n" | ||
| 28 | //usage: "c\n" | ||
| 29 | |||
| 30 | #include "libbb.h" | ||
| 31 | #include "common_bufsiz.h" | ||
| 32 | |||
| 33 | struct node { | ||
| 34 | unsigned in_count; | ||
| 35 | unsigned out_count; | ||
| 36 | struct node **out; | ||
| 37 | char name[1]; | ||
| 38 | }; | ||
| 39 | |||
| 40 | struct globals { | ||
| 41 | struct node **nodes; | ||
| 42 | unsigned nodes_len; | ||
| 43 | }; | ||
| 44 | #define G (*(struct globals*)bb_common_bufsiz1) | ||
| 45 | #define INIT_G() do { \ | ||
| 46 | setup_common_bufsiz(); \ | ||
| 47 | BUILD_BUG_ON(sizeof(G) > COMMON_BUFSIZE); \ | ||
| 48 | G.nodes = NULL; \ | ||
| 49 | G.nodes_len = 0; \ | ||
| 50 | } while (0) | ||
| 51 | |||
| 52 | static struct node * | ||
| 53 | get_node(const char *name) | ||
| 54 | { | ||
| 55 | struct node *n; | ||
| 56 | unsigned a = 0; | ||
| 57 | unsigned b = G.nodes_len; | ||
| 58 | |||
| 59 | /* Binary search for name */ | ||
| 60 | while (a != b) { | ||
| 61 | unsigned m = (a + b) / 2; | ||
| 62 | int cmp = strcmp(name, G.nodes[m]->name); | ||
| 63 | if (cmp == 0) | ||
| 64 | return G.nodes[m]; /* found */ | ||
| 65 | if (cmp < 0) { | ||
| 66 | b = m; | ||
| 67 | } else { | ||
| 68 | a = m + 1; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | /* Allocate new node */ | ||
| 73 | n = xzalloc(sizeof(*n) + strlen(name)); | ||
| 74 | //n->in_count = 0; | ||
| 75 | //n->out_count = 0; | ||
| 76 | //n->out = NULL; | ||
| 77 | strcpy(n->name, name); | ||
| 78 | |||
| 79 | /* Insert to maintain sort */ | ||
| 80 | G.nodes = xrealloc(G.nodes, (G.nodes_len + 1) * sizeof(*G.nodes)); | ||
| 81 | memmove(&G.nodes[a + 1], &G.nodes[a], | ||
| 82 | (G.nodes_len - a) * sizeof(*G.nodes)); | ||
| 83 | G.nodes[a] = n; | ||
| 84 | G.nodes_len++; | ||
| 85 | return n; | ||
| 86 | } | ||
| 87 | |||
| 88 | static void | ||
| 89 | add_edge(struct node *a, struct node *b) | ||
| 90 | { | ||
| 91 | a->out = xrealloc_vector(a->out, 6, a->out_count); | ||
| 92 | a->out[a->out_count++] = b; | ||
| 93 | b->in_count++; | ||
| 94 | } | ||
| 95 | |||
| 96 | int tsort_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; | ||
| 97 | int tsort_main(int argc UNUSED_PARAM, char **argv) | ||
| 98 | { | ||
| 99 | char *line; | ||
| 100 | size_t linesz; | ||
| 101 | ssize_t len; | ||
| 102 | struct node *a; | ||
| 103 | int cycles; | ||
| 104 | |||
| 105 | INIT_G(); | ||
| 106 | |||
| 107 | if (argv[1]) { | ||
| 108 | if (argv[2]) | ||
| 109 | bb_show_usage(); | ||
| 110 | if (NOT_LONE_DASH(argv[1])) { | ||
| 111 | close(STDIN_FILENO); /* == 0 */ | ||
| 112 | xopen(argv[1], O_RDONLY); /* fd will be 0 */ | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | /* Read in words separated by <blank>s */ | ||
| 117 | a = NULL; | ||
| 118 | line = NULL; | ||
| 119 | linesz = 0; | ||
| 120 | while ((len = getline(&line, &linesz, stdin)) != -1) { | ||
| 121 | char *s = line; | ||
| 122 | while (*(s = skip_whitespace(s)) != '\0') { | ||
| 123 | struct node *b; | ||
| 124 | char *word; | ||
| 125 | |||
| 126 | word = s; | ||
| 127 | s = skip_non_whitespace(s); | ||
| 128 | if (*s) | ||
| 129 | *s++ = '\0'; | ||
| 130 | |||
| 131 | /* Create nodes and edges for each word pair */ | ||
| 132 | b = get_node(word); | ||
| 133 | if (!a) { | ||
| 134 | a = b; | ||
| 135 | } else { | ||
| 136 | if (a != b) | ||
| 137 | add_edge(a, b); | ||
| 138 | a = NULL; | ||
| 139 | } | ||
| 140 | } | ||
| 141 | } | ||
| 142 | // Most other tools do not check for input read error (treat them as EOF) | ||
| 143 | // die_if_ferror(in, input_filename); | ||
| 144 | if (a) | ||
| 145 | bb_simple_error_msg_and_die("odd input"); | ||
| 146 | free(line); | ||
| 147 | |||
| 148 | /* | ||
| 149 | * Kahn's algorithm: | ||
| 150 | * - find a node that has no incoming edges, print and remove it | ||
| 151 | * - repeat until the graph is empty | ||
| 152 | * - if any nodes are left, they form cycles. | ||
| 153 | */ | ||
| 154 | cycles = 0; | ||
| 155 | while (G.nodes_len) { | ||
| 156 | struct node *n; | ||
| 157 | unsigned i; | ||
| 158 | |||
| 159 | /* Search for first node with no incoming edges */ | ||
| 160 | for (i = 0; i < G.nodes_len; i++) { | ||
| 161 | if (!G.nodes[i]->in_count) | ||
| 162 | break; | ||
| 163 | } | ||
| 164 | if (i == G.nodes_len) { | ||
| 165 | /* Must be a cycle; arbitraily break it at node 0 */ | ||
| 166 | cycles++; | ||
| 167 | i = 0; | ||
| 168 | #ifndef TINY | ||
| 169 | bb_error_msg("cycle at %s", G.nodes[i]->name); | ||
| 170 | #endif | ||
| 171 | } | ||
| 172 | |||
| 173 | /* Remove the node (need no longer maintain sort) */ | ||
| 174 | n = G.nodes[i]; | ||
| 175 | G.nodes[i] = G.nodes[--G.nodes_len]; | ||
| 176 | |||
| 177 | /* And remove its outgoing edges */ | ||
| 178 | for (i = 0; i < n->out_count; i++) | ||
| 179 | n->out[i]->in_count--; | ||
| 180 | free(n->out); | ||
| 181 | |||
| 182 | puts(n->name); | ||
| 183 | free(n); | ||
| 184 | } | ||
| 185 | free(G.nodes); | ||
| 186 | |||
| 187 | fflush_stdout_and_exit(cycles ? 1 : 0); | ||
| 188 | } | ||
diff --git a/docs/posix_conformance.txt b/docs/posix_conformance.txt index 5e107d74d..8edbe3e15 100644 --- a/docs/posix_conformance.txt +++ b/docs/posix_conformance.txt | |||
| @@ -24,7 +24,7 @@ POSIX Tools not supported: | |||
| 24 | gencat, getconf, iconv, join, link, locale, localedef, lp, m4, | 24 | gencat, getconf, iconv, join, link, locale, localedef, lp, m4, |
| 25 | mailx, newgrp, nl, pathchk, pax, pr, qalter, qdel, qhold, qmove, | 25 | mailx, newgrp, nl, pathchk, pax, pr, qalter, qdel, qhold, qmove, |
| 26 | qmsg, qrerun, qrls, qselect, qsig, qstat, qsub, tabs, talk, tput, | 26 | qmsg, qrerun, qrls, qselect, qsig, qstat, qsub, tabs, talk, tput, |
| 27 | tsort, unlink, uucp, uustat, uux | 27 | unlink, uucp, uustat, uux |
| 28 | 28 | ||
| 29 | POSIX Tools not supported (DEVELOPMENT): | 29 | POSIX Tools not supported (DEVELOPMENT): |
| 30 | admin, cflow, ctags, cxref, delta, fort77, get, lex, make, nm, prs, rmdel, | 30 | admin, cflow, ctags, cxref, delta, fort77, get, lex, make, nm, prs, rmdel, |
diff --git a/testsuite/tsort.tests b/testsuite/tsort.tests new file mode 100755 index 000000000..c6fe78272 --- /dev/null +++ b/testsuite/tsort.tests | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | |||
| 3 | # SUSv3 compliant sort tests. | ||
| 4 | # Public Domain, David Leonard 2022 | ||
| 5 | |||
| 6 | . ./testing.sh | ||
| 7 | |||
| 8 | # name cmd expected ./input stdin | ||
| 9 | testing "" "tsort" "a\n" "" "a a\n" | ||
| 10 | testing "" "tsort -" "a\n" "" "a a\n" | ||
| 11 | testing "" "tsort input" "a\n" "a a\n" "" | ||
| 12 | testing "tsort input (w/o eol)" "tsort input" "a\n" "a a" "" | ||
| 13 | testing "" "tsort /dev/null" "" "" "" | ||
| 14 | |||
| 15 | testing "tsort empty" tsort "" "" "" | ||
| 16 | testing "tsort blank" tsort "" "" "\n" | ||
| 17 | testing "tsort blanks" tsort "" "" "\n\n \t\n " | ||
| 18 | |||
| 19 | # simple inputs having exactly one solution | ||
| 20 | testing "tsort 1-edge" tsort "a\nb\n" "" "a b\n" | ||
| 21 | testing "tsort 2-edge" tsort "a\nb\nc\n" "" "a b b c\n" | ||
| 22 | |||
| 23 | |||
| 24 | # The following test helper accommodates future variable output because, as | ||
| 25 | # tsort is allowed to emit any total ordering that satisfies its input, | ||
| 26 | # should the implementation changes, these tests will remain valid. | ||
| 27 | # | ||
| 28 | # The idea is to verify that: | ||
| 29 | # - each input word is present EXACTLY ONCE in tsort's output | ||
| 30 | # - for each input pair 'a b', the occurrence of 'a' APPEARS BEFORE 'b' | ||
| 31 | # - the exit code is 0 | ||
| 32 | |||
| 33 | tsort_test () { | ||
| 34 | fail= | ||
| 35 | name="$1"; shift | ||
| 36 | args="$*" | ||
| 37 | if [ $VERBOSE ]; then | ||
| 38 | echo "============" | ||
| 39 | echo "echo \"$args\" | tsort >actual" | ||
| 40 | fi | ||
| 41 | echo "$args" | tsort >actual | ||
| 42 | ec=$? | ||
| 43 | if [ $ec -ne 0 ]; then | ||
| 44 | fail "tsort exit $ec, expected 0" | ||
| 45 | fi | ||
| 46 | while [ $# -ne 0 ]; do | ||
| 47 | a=$1; shift | ||
| 48 | b=$1; shift | ||
| 49 | aline=$(grep -nxF "$a" <actual | cut -d: -f1) | ||
| 50 | bline=$(grep -nxF "$b" <actual | cut -d: -f1) | ||
| 51 | case $aline in | ||
| 52 | "") fail "word $a missing from output ($args)";; | ||
| 53 | *" "*) fail "word $a duplicated ($args)";; | ||
| 54 | esac | ||
| 55 | case $bline in | ||
| 56 | "") fail "word $b missing from output ($args)";; | ||
| 57 | *" "*) fail "word $b duplicated ($args)";; | ||
| 58 | esac | ||
| 59 | if [ $aline -gt $bline ]; then | ||
| 60 | fail "$a appears after $b ($args)" | ||
| 61 | fi | ||
| 62 | done | ||
| 63 | if [ $fail ] && [ $VERBOSE ]; then | ||
| 64 | echo "exit $ec, actual:" | ||
| 65 | cat actual | ||
| 66 | fi | ||
| 67 | rm actual | ||
| 68 | report "$name" | ||
| 69 | } | ||
| 70 | |||
| 71 | # Test that erroneous input causes an unsuccessful exit code | ||
| 72 | # we don't test the output error message | ||
| 73 | tsort_test_err () { | ||
| 74 | fail= | ||
| 75 | name="$1"; shift | ||
| 76 | echo "$*" | tsort >/dev/null 2>/dev/null | ||
| 77 | ec=$? | ||
| 78 | if [ $ec -eq 0 ]; then | ||
| 79 | fail "$name: unexpected exit 0 ($*)" | ||
| 80 | fi | ||
| 81 | report "$name" | ||
| 82 | } | ||
| 83 | |||
| 84 | fail () { | ||
| 85 | [ $VERBOSE ] && echo "ERROR: $*" | ||
| 86 | fail=1 | ||
| 87 | } | ||
| 88 | |||
| 89 | report () { | ||
| 90 | if [ $fail ]; then | ||
| 91 | FAILCOUNT=$(($FAILCOUNT + 1)) | ||
| 92 | echo "FAIL: $*" | ||
| 93 | else | ||
| 94 | echo "PASS: $*" | ||
| 95 | fi | ||
| 96 | } | ||
| 97 | |||
| 98 | tsort_test "tsort empty2" | ||
| 99 | tsort_test "tsort singleton" a a | ||
| 100 | tsort_test "tsort simple" a b b c | ||
| 101 | tsort_test "tsort 2singleton" a a b b | ||
| 102 | tsort_test "tsort medium" a b a b b c | ||
| 103 | tsort_test "tsort std.example" a b c c d e g g f g e f h h | ||
| 104 | tsort_test "tsort prefixes" a aa aa aaa aaaa aaaaa a aaaaa | ||
| 105 | |||
| 106 | tsort_test_err "tsort odd" a | ||
| 107 | tsort_test_err "tsort odd2" a b c | ||
| 108 | tsort_test_err "tsort cycle" a b b a | ||
| 109 | |||
| 110 | exit $FAILCOUNT | ||
