Commit 7ed991343b0d6d4f4ec84cf41dde60debffc4074
1 parent
f545f8b0
Better diagnostics when --pages is not closed (fixes #555)
Showing
4 changed files
with
71 additions
and
24 deletions
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 | 2021-05-12 Jay Berkenbilt <ejb@ql.org> | 6 | 2021-05-12 Jay Berkenbilt <ejb@ql.org> |
| 2 | 7 | ||
| 3 | * Bug fix: ensure we don't overflow any string bounds while | 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& objspec, | @@ -738,6 +738,21 @@ static void parse_object_id(std::string const& 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 | // This is not a general-purpose argument parser. It is tightly | 756 | // This is not a general-purpose argument parser. It is tightly |
| 742 | // crafted to work with qpdf. qpdf's command-line syntax is very | 757 | // crafted to work with qpdf. qpdf's command-line syntax is very |
| 743 | // complex because of its long history, and it doesn't really follow | 758 | // complex because of its long history, and it doesn't really follow |
| @@ -2080,6 +2095,10 @@ void | @@ -2080,6 +2095,10 @@ void | ||
| 2080 | ArgParser::argPages() | 2095 | ArgParser::argPages() |
| 2081 | { | 2096 | { |
| 2082 | ++cur_arg; | 2097 | ++cur_arg; |
| 2098 | + if (! o.page_specs.empty()) | ||
| 2099 | + { | ||
| 2100 | + usage("the --pages may only be specified one time"); | ||
| 2101 | + } | ||
| 2083 | o.page_specs = parsePagesOptions(); | 2102 | o.page_specs = parsePagesOptions(); |
| 2084 | if (o.page_specs.empty()) | 2103 | if (o.page_specs.empty()) |
| 2085 | { | 2104 | { |
| @@ -2937,19 +2956,15 @@ ArgParser::handleArgFileArguments() | @@ -2937,19 +2956,15 @@ ArgParser::handleArgFileArguments() | ||
| 2937 | char* argfile = 0; | 2956 | char* argfile = 0; |
| 2938 | if ((strlen(argv[i]) > 1) && (argv[i][0] == '@')) | 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 | if (argfile) | 2969 | if (argfile) |
| 2955 | { | 2970 | { |
| @@ -3220,6 +3235,16 @@ ArgParser::parseNumrange(char const* range, int max, bool throw_error) | @@ -3220,6 +3235,16 @@ ArgParser::parseNumrange(char const* range, int max, bool throw_error) | ||
| 3220 | std::vector<PageSpec> | 3235 | std::vector<PageSpec> |
| 3221 | ArgParser::parsePagesOptions() | 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 | std::vector<PageSpec> result; | 3248 | std::vector<PageSpec> result; |
| 3224 | while (1) | 3249 | while (1) |
| 3225 | { | 3250 | { |
| @@ -3234,6 +3259,10 @@ ArgParser::parsePagesOptions() | @@ -3234,6 +3259,10 @@ ArgParser::parsePagesOptions() | ||
| 3234 | char const* file = argv[cur_arg++]; | 3259 | char const* file = argv[cur_arg++]; |
| 3235 | char const* password = 0; | 3260 | char const* password = 0; |
| 3236 | char const* range = argv[cur_arg++]; | 3261 | char const* range = argv[cur_arg++]; |
| 3262 | + if (! file_exists(file)) | ||
| 3263 | + { | ||
| 3264 | + check_unclosed(file, 0); | ||
| 3265 | + } | ||
| 3237 | if (strncmp(range, "--password=", 11) == 0) | 3266 | if (strncmp(range, "--password=", 11) == 0) |
| 3238 | { | 3267 | { |
| 3239 | // Oh, that's the password, not the range | 3268 | // Oh, that's the password, not the range |
| @@ -3263,23 +3292,20 @@ ArgParser::parsePagesOptions() | @@ -3263,23 +3292,20 @@ ArgParser::parsePagesOptions() | ||
| 3263 | catch (std::runtime_error& e1) | 3292 | catch (std::runtime_error& e1) |
| 3264 | { | 3293 | { |
| 3265 | // The range is invalid. Let's see if it's a file. | 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 | // Give the range error | 3309 | // Give the range error |
| 3284 | usage(e1.what()); | 3310 | usage(e1.what()); |
| 3285 | } | 3311 | } |
qpdf/qpdf.testcov
| @@ -594,3 +594,4 @@ qpdf copy fields non-first from orig 0 | @@ -594,3 +594,4 @@ qpdf copy fields non-first from orig 0 | ||
| 594 | QPDF resolve duplicated page in insert 0 | 594 | QPDF resolve duplicated page in insert 0 |
| 595 | QPDFWriter preserve object streams 1 | 595 | QPDFWriter preserve object streams 1 |
| 596 | QPDFWriter exclude from object stream 0 | 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,7 +139,7 @@ foreach my $c (@completion_tests) | ||
| 139 | show_ntests(); | 139 | show_ntests(); |
| 140 | # ---------- | 140 | # ---------- |
| 141 | $td->notify("--- Argument Parsing ---"); | 141 | $td->notify("--- Argument Parsing ---"); |
| 142 | -$n_tests += 6; | 142 | +$n_tests += 9; |
| 143 | 143 | ||
| 144 | $td->runtest("required argument", | 144 | $td->runtest("required argument", |
| 145 | {$td->COMMAND => "qpdf --password minimal.pdf"}, | 145 | {$td->COMMAND => "qpdf --password minimal.pdf"}, |
| @@ -171,6 +171,21 @@ $td->runtest("extra overlay filename", | @@ -171,6 +171,21 @@ $td->runtest("extra overlay filename", | ||
| 171 | {$td->REGEXP => ".*overlay file already specified.*", | 171 | {$td->REGEXP => ".*overlay file already specified.*", |
| 172 | $td->EXIT_STATUS => 2}, | 172 | $td->EXIT_STATUS => 2}, |
| 173 | $td->NORMALIZE_NEWLINES); | 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 | show_ntests(); | 190 | show_ntests(); |
| 176 | # ---------- | 191 | # ---------- |