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 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&amp; objspec, @@ -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 // 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-&gt;runtest(&quot;extra overlay filename&quot;, @@ -171,6 +171,21 @@ $td-&gt;runtest(&quot;extra overlay filename&quot;,
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 # ----------