Commit 7ed991343b0d6d4f4ec84cf41dde60debffc4074

Authored by Jay Berkenbilt
1 parent f545f8b0

Better diagnostics when --pages is not closed (fixes #555)

ChangeLog
  1 +2021-11-02 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Improve error reporting when someone forgets the -- after
  4 + --pages. Fixes #555.
  5 +
1 6 2021-05-12 Jay Berkenbilt <ejb@ql.org>
2 7  
3 8 * Bug fix: ensure we don't overflow any string bounds while
... ...
qpdf/qpdf.cc
... ... @@ -738,6 +738,21 @@ static void parse_object_id(std::string const&amp; objspec,
738 738 }
739 739 }
740 740  
  741 +static bool file_exists(char const* filename)
  742 +{
  743 + try
  744 + {
  745 + fclose(QUtil::safe_fopen(filename, "rb"));
  746 + return true;
  747 + }
  748 + catch (std::runtime_error&)
  749 + {
  750 + // can't open the file
  751 + }
  752 + return false;
  753 +}
  754 +
  755 +
741 756 // This is not a general-purpose argument parser. It is tightly
742 757 // crafted to work with qpdf. qpdf's command-line syntax is very
743 758 // complex because of its long history, and it doesn't really follow
... ... @@ -2080,6 +2095,10 @@ void
2080 2095 ArgParser::argPages()
2081 2096 {
2082 2097 ++cur_arg;
  2098 + if (! o.page_specs.empty())
  2099 + {
  2100 + usage("the --pages may only be specified one time");
  2101 + }
2083 2102 o.page_specs = parsePagesOptions();
2084 2103 if (o.page_specs.empty())
2085 2104 {
... ... @@ -2937,19 +2956,15 @@ ArgParser::handleArgFileArguments()
2937 2956 char* argfile = 0;
2938 2957 if ((strlen(argv[i]) > 1) && (argv[i][0] == '@'))
2939 2958 {
2940   - try
  2959 + argfile = 1 + argv[i];
  2960 + if (strcmp(argfile, "-") != 0)
2941 2961 {
2942   - argfile = 1 + argv[i];
2943   - if (strcmp(argfile, "-") != 0)
  2962 + if (! file_exists(argfile))
2944 2963 {
2945   - fclose(QUtil::safe_fopen(argfile, "rb"));
  2964 + // The file's not there; treating as regular option
  2965 + argfile = nullptr;
2946 2966 }
2947 2967 }
2948   - catch (std::runtime_error&)
2949   - {
2950   - // The file's not there; treating as regular option
2951   - argfile = 0;
2952   - }
2953 2968 }
2954 2969 if (argfile)
2955 2970 {
... ... @@ -3220,6 +3235,16 @@ ArgParser::parseNumrange(char const* range, int max, bool throw_error)
3220 3235 std::vector<PageSpec>
3221 3236 ArgParser::parsePagesOptions()
3222 3237 {
  3238 + auto check_unclosed = [this](char const* arg, int n) {
  3239 + if ((strlen(arg) > 0) && (arg[0] == '-'))
  3240 + {
  3241 + // A common error is to forget to close --pages with --,
  3242 + // so catch this as special case
  3243 + QTC::TC("qpdf", "check unclosed --pages", n);
  3244 + usage("the --pages option must be terminated with -- by itself");
  3245 + }
  3246 + };
  3247 +
3223 3248 std::vector<PageSpec> result;
3224 3249 while (1)
3225 3250 {
... ... @@ -3234,6 +3259,10 @@ ArgParser::parsePagesOptions()
3234 3259 char const* file = argv[cur_arg++];
3235 3260 char const* password = 0;
3236 3261 char const* range = argv[cur_arg++];
  3262 + if (! file_exists(file))
  3263 + {
  3264 + check_unclosed(file, 0);
  3265 + }
3237 3266 if (strncmp(range, "--password=", 11) == 0)
3238 3267 {
3239 3268 // Oh, that's the password, not the range
... ... @@ -3263,23 +3292,20 @@ ArgParser::parsePagesOptions()
3263 3292 catch (std::runtime_error& e1)
3264 3293 {
3265 3294 // The range is invalid. Let's see if it's a file.
3266   - try
  3295 + range_omitted = true;
  3296 + if (strcmp(range, ".") == 0)
3267 3297 {
3268   - if (strcmp(range, ".") == 0)
3269   - {
3270   - // "." means the input file.
3271   - QTC::TC("qpdf", "qpdf pages range omitted with .");
3272   - }
3273   - else
3274   - {
3275   - fclose(QUtil::safe_fopen(range, "rb"));
3276   - QTC::TC("qpdf", "qpdf pages range omitted in middle");
3277   - // Yup, it's a file.
3278   - }
3279   - range_omitted = true;
  3298 + // "." means the input file.
  3299 + QTC::TC("qpdf", "qpdf pages range omitted with .");
3280 3300 }
3281   - catch (std::runtime_error&)
  3301 + else if (file_exists(range))
  3302 + {
  3303 + QTC::TC("qpdf", "qpdf pages range omitted in middle");
  3304 + // Yup, it's a file.
  3305 + }
  3306 + else
3282 3307 {
  3308 + check_unclosed(range, 1);
3283 3309 // Give the range error
3284 3310 usage(e1.what());
3285 3311 }
... ...
qpdf/qpdf.testcov
... ... @@ -594,3 +594,4 @@ qpdf copy fields non-first from orig 0
594 594 QPDF resolve duplicated page in insert 0
595 595 QPDFWriter preserve object streams 1
596 596 QPDFWriter exclude from object stream 0
  597 +check unclosed --pages 1
... ...
qpdf/qtest/qpdf.test
... ... @@ -139,7 +139,7 @@ foreach my $c (@completion_tests)
139 139 show_ntests();
140 140 # ----------
141 141 $td->notify("--- Argument Parsing ---");
142   -$n_tests += 6;
  142 +$n_tests += 9;
143 143  
144 144 $td->runtest("required argument",
145 145 {$td->COMMAND => "qpdf --password minimal.pdf"},
... ... @@ -171,6 +171,21 @@ $td-&gt;runtest(&quot;extra overlay filename&quot;,
171 171 {$td->REGEXP => ".*overlay file already specified.*",
172 172 $td->EXIT_STATUS => 2},
173 173 $td->NORMALIZE_NEWLINES);
  174 +$td->runtest("multiple pages options",
  175 + {$td->COMMAND => "qpdf --pages . -- --pages . --"},
  176 + {$td->REGEXP => ".*--pages may only be specified one time.*",
  177 + $td->EXIT_STATUS => 2},
  178 + $td->NORMALIZE_NEWLINES);
  179 +$td->runtest("bad numeric range detects unclosed --pages",
  180 + {$td->COMMAND => "qpdf --pages . --pages . --"},
  181 + {$td->REGEXP => ".*--pages option must be terminated with --.*",
  182 + $td->EXIT_STATUS => 2},
  183 + $td->NORMALIZE_NEWLINES);
  184 +$td->runtest("bad file detected as unclosed --pages",
  185 + {$td->COMMAND => "qpdf --pages . 1 --xyz out"},
  186 + {$td->REGEXP => ".*--pages option must be terminated with --.*",
  187 + $td->EXIT_STATUS => 2},
  188 + $td->NORMALIZE_NEWLINES);
174 189  
175 190 show_ntests();
176 191 # ----------
... ...