Commit fa3664357b6fd23b6d74c6835bbf6c8e911892aa

Authored by Jay Berkenbilt
1 parent 313ba081

Move numrange code from qpdf.cc to QUtil.cc

Also move tests to libtests.
include/qpdf/QUtil.hh
@@ -26,6 +26,7 @@ @@ -26,6 +26,7 @@
26 #include <qpdf/Types.h> 26 #include <qpdf/Types.h>
27 #include <string> 27 #include <string>
28 #include <list> 28 #include <list>
  29 +#include <vector>
29 #include <stdexcept> 30 #include <stdexcept>
30 #include <stdio.h> 31 #include <stdio.h>
31 #include <time.h> 32 #include <time.h>
@@ -220,6 +221,11 @@ namespace QUtil @@ -220,6 +221,11 @@ namespace QUtil
220 221
221 QPDF_DLL 222 QPDF_DLL
222 bool is_number(char const*); 223 bool is_number(char const*);
  224 +
  225 + // This method parses the numeric range syntax used by the qpdf
  226 + // command-line tool. May throw std::runtime_error.
  227 + QPDF_DLL
  228 + std::vector<int> parse_numrange(char const* range, int max);
223 }; 229 };
224 230
225 #endif // QUTIL_HH 231 #endif // QUTIL_HH
libqpdf/QUtil.cc
@@ -718,3 +718,177 @@ QUtil::strcasecmp(char const *s1, char const *s2) @@ -718,3 +718,177 @@ QUtil::strcasecmp(char const *s1, char const *s2)
718 return ::strcasecmp(s1, s2); 718 return ::strcasecmp(s1, s2);
719 #endif 719 #endif
720 } 720 }
  721 +
  722 +static int maybe_from_end(int num, bool from_end, int max)
  723 +{
  724 + if (from_end)
  725 + {
  726 + if (num > max)
  727 + {
  728 + num = 0;
  729 + }
  730 + else
  731 + {
  732 + num = max + 1 - num;
  733 + }
  734 + }
  735 + return num;
  736 +}
  737 +
  738 +std::vector<int>
  739 +QUtil::parse_numrange(char const* range, int max)
  740 +{
  741 + std::vector<int> result;
  742 + char const* p = range;
  743 + try
  744 + {
  745 + std::vector<int> work;
  746 + static int const comma = -1;
  747 + static int const dash = -2;
  748 +
  749 + enum { st_top,
  750 + st_in_number,
  751 + st_after_number } state = st_top;
  752 + bool last_separator_was_dash = false;
  753 + int cur_number = 0;
  754 + bool from_end = false;
  755 + while (*p)
  756 + {
  757 + char ch = *p;
  758 + if (isdigit(ch))
  759 + {
  760 + if (! ((state == st_top) || (state == st_in_number)))
  761 + {
  762 + throw std::runtime_error("digit not expected");
  763 + }
  764 + state = st_in_number;
  765 + cur_number *= 10;
  766 + cur_number += (ch - '0');
  767 + }
  768 + else if (ch == 'z')
  769 + {
  770 + // z represents max
  771 + if (! (state == st_top))
  772 + {
  773 + throw std::runtime_error("z not expected");
  774 + }
  775 + state = st_after_number;
  776 + cur_number = max;
  777 + }
  778 + else if (ch == 'r')
  779 + {
  780 + if (! (state == st_top))
  781 + {
  782 + throw std::runtime_error("r not expected");
  783 + }
  784 + state = st_in_number;
  785 + from_end = true;
  786 + }
  787 + else if ((ch == ',') || (ch == '-'))
  788 + {
  789 + if (! ((state == st_in_number) || (state == st_after_number)))
  790 + {
  791 + throw std::runtime_error("unexpected separator");
  792 + }
  793 + cur_number = maybe_from_end(cur_number, from_end, max);
  794 + work.push_back(cur_number);
  795 + cur_number = 0;
  796 + from_end = false;
  797 + if (ch == ',')
  798 + {
  799 + state = st_top;
  800 + last_separator_was_dash = false;
  801 + work.push_back(comma);
  802 + }
  803 + else if (ch == '-')
  804 + {
  805 + if (last_separator_was_dash)
  806 + {
  807 + throw std::runtime_error("unexpected dash");
  808 + }
  809 + state = st_top;
  810 + last_separator_was_dash = true;
  811 + work.push_back(dash);
  812 + }
  813 + }
  814 + else
  815 + {
  816 + throw std::runtime_error("unexpected character");
  817 + }
  818 + ++p;
  819 + }
  820 + if ((state == st_in_number) || (state == st_after_number))
  821 + {
  822 + cur_number = maybe_from_end(cur_number, from_end, max);
  823 + work.push_back(cur_number);
  824 + }
  825 + else
  826 + {
  827 + throw std::runtime_error("number expected");
  828 + }
  829 +
  830 + p = 0;
  831 + for (size_t i = 0; i < work.size(); i += 2)
  832 + {
  833 + int num = work.at(i);
  834 + // max == 0 means we don't know the max and are just
  835 + // testing for valid syntax.
  836 + if ((max > 0) && ((num < 1) || (num > max)))
  837 + {
  838 + throw std::runtime_error(
  839 + "number " + QUtil::int_to_string(num) + " out of range");
  840 + }
  841 + if (i == 0)
  842 + {
  843 + result.push_back(work.at(i));
  844 + }
  845 + else
  846 + {
  847 + int separator = work.at(i-1);
  848 + if (separator == comma)
  849 + {
  850 + result.push_back(num);
  851 + }
  852 + else if (separator == dash)
  853 + {
  854 + int lastnum = result.back();
  855 + if (num > lastnum)
  856 + {
  857 + for (int j = lastnum + 1; j <= num; ++j)
  858 + {
  859 + result.push_back(j);
  860 + }
  861 + }
  862 + else
  863 + {
  864 + for (int j = lastnum - 1; j >= num; --j)
  865 + {
  866 + result.push_back(j);
  867 + }
  868 + }
  869 + }
  870 + else
  871 + {
  872 + throw std::logic_error(
  873 + "INTERNAL ERROR parsing numeric range");
  874 + }
  875 + }
  876 + }
  877 + }
  878 + catch (std::runtime_error const& e)
  879 + {
  880 + std::string message;
  881 + if (p)
  882 + {
  883 + message = "error at * in numeric range " +
  884 + std::string(range, p - range) + "*" + p + ": " + e.what();
  885 + }
  886 + else
  887 + {
  888 + message = "error in numeric range " +
  889 + std::string(range) + ": " + e.what();
  890 + }
  891 + throw std::runtime_error(message);
  892 + }
  893 + return result;
  894 +}
libtests/build.mk
@@ -13,6 +13,7 @@ BINS_libtests = \ @@ -13,6 +13,7 @@ BINS_libtests = \
13 json \ 13 json \
14 lzw \ 14 lzw \
15 md5 \ 15 md5 \
  16 + numrange \
16 pointer_holder \ 17 pointer_holder \
17 predictors \ 18 predictors \
18 qutil \ 19 qutil \
libtests/numrange.cc 0 โ†’ 100644
  1 +#include <qpdf/QUtil.hh>
  2 +#include <iostream>
  3 +
  4 +static void test_numrange(char const* range)
  5 +{
  6 + if (range == 0)
  7 + {
  8 + std::cout << "null" << std::endl;
  9 + }
  10 + else
  11 + {
  12 + std::vector<int> result = QUtil::parse_numrange(range, 15);
  13 + std::cout << "numeric range " << range << " ->";
  14 + for (std::vector<int>::iterator iter = result.begin();
  15 + iter != result.end(); ++iter)
  16 + {
  17 + std::cout << " " << *iter;
  18 + }
  19 + std::cout << std::endl;
  20 + }
  21 +}
  22 +
  23 +int main(int argc, char* argv[])
  24 +{
  25 + try
  26 + {
  27 + test_numrange(argv[1]);
  28 + }
  29 + catch (std::exception& e)
  30 + {
  31 + std::cout << e.what() << std::endl;
  32 + return 2;
  33 + }
  34 +
  35 + return 0;
  36 +}
libtests/qtest/numrange.test 0 โ†’ 100644
  1 +#!/usr/bin/env perl
  2 +require 5.008;
  3 +use warnings;
  4 +use strict;
  5 +
  6 +require TestDriver;
  7 +
  8 +my $td = new TestDriver('numrange');
  9 +
  10 +my @nrange_tests = (
  11 + [",5",
  12 + "error at * in numeric range *,5: unexpected separator",
  13 + 2],
  14 + ["4,,5",
  15 + "error at * in numeric range 4,*,5: unexpected separator",
  16 + 2],
  17 + ["4,5,",
  18 + "error at * in numeric range 4,5,*: number expected",
  19 + 2],
  20 + ["z1,",
  21 + "error at * in numeric range z*1,: digit not expected",
  22 + 2],
  23 + ["1z,",
  24 + "error at * in numeric range 1*z,: z not expected",
  25 + 2],
  26 + ["1-5?",
  27 + "error at * in numeric range 1-5*?: unexpected character",
  28 + 2],
  29 + ["1-30",
  30 + "error in numeric range 1-30: number 30 out of range",
  31 + 2],
  32 + ["1-10,0,5",
  33 + "error in numeric range 1-10,0,5: number 0 out of range",
  34 + 2],
  35 + ["1-10,1234,5",
  36 + "error in numeric range 1-10,1234,5: number 1234 out of range",
  37 + 2],
  38 + ["1,r,3",
  39 + "error in numeric range 1,r,3: number 16 out of range",
  40 + 2],
  41 + ["1,r16,3",
  42 + "error in numeric range 1,r16,3: number 0 out of range",
  43 + 2],
  44 + ["1,3,5-10,z-13,13,9,z,2,r2-r4",
  45 + "numeric range 1,3,5-10,z-13,13,9,z,2,r2-r4" .
  46 + " -> 1 3 5 6 7 8 9 10 15 14 13 13 9 15 2 14 13 12",
  47 + 0],
  48 + ["r1-r15", # r\d+ at end
  49 + "numeric range r1-r15" .
  50 + " -> 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1",
  51 + 0],
  52 + );
  53 +foreach my $d (@nrange_tests)
  54 +{
  55 + my ($range, $output, $status) = @$d;
  56 + $td->runtest("numeric range $range",
  57 + {$td->COMMAND => ['numrange', $range],
  58 + $td->FILTER => "grep 'numeric range'"},
  59 + {$td->STRING => $output . "\n", $td->EXIT_STATUS => $status},
  60 + $td->NORMALIZE_NEWLINES);
  61 +}
  62 +
  63 +$td->report(scalar(@nrange_tests));
qpdf/qpdf.cc
@@ -635,180 +635,25 @@ static void show_encryption(QPDF&amp; pdf, Options&amp; o) @@ -635,180 +635,25 @@ static void show_encryption(QPDF&amp; pdf, Options&amp; o)
635 } 635 }
636 } 636 }
637 637
638 -static int maybe_from_end(int num, bool from_end, int max)  
639 -{  
640 - if (from_end)  
641 - {  
642 - if (num > max)  
643 - {  
644 - num = 0;  
645 - }  
646 - else  
647 - {  
648 - num = max + 1 - num;  
649 - }  
650 - }  
651 - return num;  
652 -}  
653 -  
654 static std::vector<int> parse_numrange(char const* range, int max, 638 static std::vector<int> parse_numrange(char const* range, int max,
655 bool throw_error = false) 639 bool throw_error = false)
656 { 640 {
657 - std::vector<int> result;  
658 - char const* p = range;  
659 try 641 try
660 { 642 {
661 - std::vector<int> work;  
662 - static int const comma = -1;  
663 - static int const dash = -2;  
664 -  
665 - enum { st_top,  
666 - st_in_number,  
667 - st_after_number } state = st_top;  
668 - bool last_separator_was_dash = false;  
669 - int cur_number = 0;  
670 - bool from_end = false;  
671 - while (*p)  
672 - {  
673 - char ch = *p;  
674 - if (isdigit(ch))  
675 - {  
676 - if (! ((state == st_top) || (state == st_in_number)))  
677 - {  
678 - throw std::runtime_error("digit not expected");  
679 - }  
680 - state = st_in_number;  
681 - cur_number *= 10;  
682 - cur_number += (ch - '0');  
683 - }  
684 - else if (ch == 'z')  
685 - {  
686 - // z represents max  
687 - if (! (state == st_top))  
688 - {  
689 - throw std::runtime_error("z not expected");  
690 - }  
691 - state = st_after_number;  
692 - cur_number = max;  
693 - }  
694 - else if (ch == 'r')  
695 - {  
696 - if (! (state == st_top))  
697 - {  
698 - throw std::runtime_error("r not expected");  
699 - }  
700 - state = st_in_number;  
701 - from_end = true;  
702 - }  
703 - else if ((ch == ',') || (ch == '-'))  
704 - {  
705 - if (! ((state == st_in_number) || (state == st_after_number)))  
706 - {  
707 - throw std::runtime_error("unexpected separator");  
708 - }  
709 - cur_number = maybe_from_end(cur_number, from_end, max);  
710 - work.push_back(cur_number);  
711 - cur_number = 0;  
712 - from_end = false;  
713 - if (ch == ',')  
714 - {  
715 - state = st_top;  
716 - last_separator_was_dash = false;  
717 - work.push_back(comma);  
718 - }  
719 - else if (ch == '-')  
720 - {  
721 - if (last_separator_was_dash)  
722 - {  
723 - throw std::runtime_error("unexpected dash");  
724 - }  
725 - state = st_top;  
726 - last_separator_was_dash = true;  
727 - work.push_back(dash);  
728 - }  
729 - }  
730 - else  
731 - {  
732 - throw std::runtime_error("unexpected character");  
733 - }  
734 - ++p;  
735 - }  
736 - if ((state == st_in_number) || (state == st_after_number))  
737 - {  
738 - cur_number = maybe_from_end(cur_number, from_end, max);  
739 - work.push_back(cur_number);  
740 - }  
741 - else  
742 - {  
743 - throw std::runtime_error("number expected");  
744 - }  
745 -  
746 - p = 0;  
747 - for (size_t i = 0; i < work.size(); i += 2)  
748 - {  
749 - int num = work.at(i);  
750 - // max == 0 means we don't know the max and are just  
751 - // testing for valid syntax.  
752 - if ((max > 0) && ((num < 1) || (num > max)))  
753 - {  
754 - throw std::runtime_error(  
755 - "number " + QUtil::int_to_string(num) + " out of range");  
756 - }  
757 - if (i == 0)  
758 - {  
759 - result.push_back(work.at(i));  
760 - }  
761 - else  
762 - {  
763 - int separator = work.at(i-1);  
764 - if (separator == comma)  
765 - {  
766 - result.push_back(num);  
767 - }  
768 - else if (separator == dash)  
769 - {  
770 - int lastnum = result.back();  
771 - if (num > lastnum)  
772 - {  
773 - for (int j = lastnum + 1; j <= num; ++j)  
774 - {  
775 - result.push_back(j);  
776 - }  
777 - }  
778 - else  
779 - {  
780 - for (int j = lastnum - 1; j >= num; --j)  
781 - {  
782 - result.push_back(j);  
783 - }  
784 - }  
785 - }  
786 - else  
787 - {  
788 - throw std::logic_error(  
789 - "INTERNAL ERROR parsing numeric range");  
790 - }  
791 - }  
792 - } 643 + return QUtil::parse_numrange(range, max);
793 } 644 }
794 - catch (std::runtime_error const& e) 645 + catch (std::runtime_error& e)
795 { 646 {
796 if (throw_error) 647 if (throw_error)
797 { 648 {
798 - throw e;  
799 - }  
800 - if (p)  
801 - {  
802 - usage("error at * in numeric range " +  
803 - std::string(range, p - range) + "*" + p + ": " + e.what()); 649 + throw(e);
804 } 650 }
805 else 651 else
806 { 652 {
807 - usage("error in numeric range " +  
808 - std::string(range) + ": " + e.what()); 653 + usage(e.what());
809 } 654 }
810 } 655 }
811 - return result; 656 + return std::vector<int>();
812 } 657 }
813 658
814 static void 659 static void
@@ -1213,25 +1058,6 @@ parse_pages_options( @@ -1213,25 +1058,6 @@ parse_pages_options(
1213 return result; 1058 return result;
1214 } 1059 }
1215 1060
1216 -static void test_numrange(char const* range)  
1217 -{  
1218 - if (range == 0)  
1219 - {  
1220 - std::cout << "null" << std::endl;  
1221 - }  
1222 - else  
1223 - {  
1224 - std::vector<int> result = parse_numrange(range, 15);  
1225 - std::cout << "numeric range " << range << " ->";  
1226 - for (std::vector<int>::iterator iter = result.begin();  
1227 - iter != result.end(); ++iter)  
1228 - {  
1229 - std::cout << " " << *iter;  
1230 - }  
1231 - std::cout << std::endl;  
1232 - }  
1233 -}  
1234 -  
1235 QPDFPageData::QPDFPageData(std::string const& filename, 1061 QPDFPageData::QPDFPageData(std::string const& filename,
1236 QPDF* qpdf, 1062 QPDF* qpdf,
1237 char const* range) : 1063 char const* range) :
@@ -1429,14 +1255,7 @@ static void parse_options(int argc, char* argv[], Options&amp; o) @@ -1429,14 +1255,7 @@ static void parse_options(int argc, char* argv[], Options&amp; o)
1429 *parameter++ = 0; 1255 *parameter++ = 0;
1430 } 1256 }
1431 1257
1432 - // Arguments that start with space are undocumented and  
1433 - // are for use by the test suite.  
1434 - if (strcmp(arg, " test-numrange") == 0)  
1435 - {  
1436 - test_numrange(parameter);  
1437 - exit(0);  
1438 - }  
1439 - else if (strcmp(arg, "password") == 0) 1258 + if (strcmp(arg, "password") == 0)
1440 { 1259 {
1441 if (parameter == 0) 1260 if (parameter == 0)
1442 { 1261 {
qpdf/qtest/qpdf.test
@@ -1321,63 +1321,6 @@ $td-&gt;runtest(&quot;check output&quot;, @@ -1321,63 +1321,6 @@ $td-&gt;runtest(&quot;check output&quot;,
1321 1321
1322 show_ntests(); 1322 show_ntests();
1323 # ---------- 1323 # ----------
1324 -$td->notify("--- Numeric range parsing tests ---");  
1325 -my @nrange_tests = (  
1326 - [",5",  
1327 - "qpdf: error at * in numeric range *,5: unexpected separator",  
1328 - 2],  
1329 - ["4,,5",  
1330 - "qpdf: error at * in numeric range 4,*,5: unexpected separator",  
1331 - 2],  
1332 - ["4,5,",  
1333 - "qpdf: error at * in numeric range 4,5,*: number expected",  
1334 - 2],  
1335 - ["z1,",  
1336 - "qpdf: error at * in numeric range z*1,: digit not expected",  
1337 - 2],  
1338 - ["1z,",  
1339 - "qpdf: error at * in numeric range 1*z,: z not expected",  
1340 - 2],  
1341 - ["1-5?",  
1342 - "qpdf: error at * in numeric range 1-5*?: unexpected character",  
1343 - 2],  
1344 - ["1-30",  
1345 - "qpdf: error in numeric range 1-30: number 30 out of range",  
1346 - 2],  
1347 - ["1-10,0,5",  
1348 - "qpdf: error in numeric range 1-10,0,5: number 0 out of range",  
1349 - 2],  
1350 - ["1-10,1234,5",  
1351 - "qpdf: error in numeric range 1-10,1234,5: number 1234 out of range",  
1352 - 2],  
1353 - ["1,r,3",  
1354 - "qpdf: error in numeric range 1,r,3: number 16 out of range",  
1355 - 2],  
1356 - ["1,r16,3",  
1357 - "qpdf: error in numeric range 1,r16,3: number 0 out of range",  
1358 - 2],  
1359 - ["1,3,5-10,z-13,13,9,z,2,r2-r4",  
1360 - "numeric range 1,3,5-10,z-13,13,9,z,2,r2-r4" .  
1361 - " -> 1 3 5 6 7 8 9 10 15 14 13 13 9 15 2 14 13 12",  
1362 - 0],  
1363 - ["r1-r15", # r\d+ at end  
1364 - "numeric range r1-r15" .  
1365 - " -> 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1",  
1366 - 0],  
1367 - );  
1368 -$n_tests += scalar(@nrange_tests);  
1369 -foreach my $d (@nrange_tests)  
1370 -{  
1371 - my ($range, $output, $status) = @$d;  
1372 - $td->runtest("numeric range $range",  
1373 - {$td->COMMAND => ['qpdf', '-- test-numrange=' . $range],  
1374 - $td->FILTER => "grep 'numeric range'"},  
1375 - {$td->STRING => $output . "\n", $td->EXIT_STATUS => $status},  
1376 - $td->NORMALIZE_NEWLINES);  
1377 -}  
1378 -  
1379 -show_ntests();  
1380 -# ----------  
1381 $td->notify("--- Merging and Splitting ---"); 1324 $td->notify("--- Merging and Splitting ---");
1382 $n_tests += 18; 1325 $n_tests += 18;
1383 1326