summaryrefslogtreecommitdiff
path: root/pkgtools/pkglint4/files/PkgLint/Patches.pm
blob: b96e7daa5f6ed5c9b061f2f28bc73a0a8102dbae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# $NetBSD: Patches.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $
#
# Everything concerning checks for patch files.
#

use strict;
use warnings;

# Guess the type of file based on the filename. This is used to select
# the proper subroutine for detecting absolute pathnames.
#
# Returns one of "source", "shell", "make", "text", "configure",
# "ignore", "unknown".
#
sub get_filetype($$) {
	my ($line, $fname) = @_;
	my $basename = basename($fname);

	# The trailig .in part is not needed, since it does not
	# influence the type of contents.
	$basename =~ s,\.in$,,;

	# Let's assume that everything else that looks like a Makefile
	# is indeed a Makefile.
	if ($basename =~ m"^I?[Mm]akefile(?:\..*|)?|.*\.ma?k$") {
		return "make";
	}

	# Too many false positives for shell scripts, so configure
	# scripts get their own category.
	if ($basename =~ m"^configure(?:|\.ac)$") {
		$opt_debug_unchecked and $line->log_debug("Skipped check for absolute pathnames.");
		return "configure";
	}

	if ($basename =~ m"\.(?:sh|m4)$"i) {
		return "shell";
	}

	if ($basename =~ m"\.(?:cc?|cpp|cxx|el|hh?|hpp|l|pl|pm|py|s|t|y)$"i) {
		return "source";
	}

	if ($basename =~ m"^.+\.(?:\d+|conf|html|info|man|po|tex|texi|texinfo|txt|xml)$"i) {
		return "text";
	}

	# Filenames without extension are hard to guess right. :(
	if ($basename !~ m"\.") {
		return "unknown";
	}

	$opt_debug_misc and $line->log_debug("Don't know the file type of ${fname}.");

	return "unknown";
}

sub checkline_cpp_macro_names($$) {
	my ($line, $text) = @_;
	my ($rest);

	use constant good_macros => PkgLint::Util::array_to_hash(qw(
		__STDC__

		__GNUC__ __GNUC_MINOR__
		__SUNPRO_C

		__i386
		__mips
		__sparc

		__APPLE__
		__bsdi__
		__CYGWIN__
		__DragonFly__
		__FreeBSD__ __FreeBSD_version
		__INTERIX
		__linux__
		__MINGW32__
		__NetBSD__ __NetBSD_Version__
		__OpenBSD__
		__SVR4
		__sgi
		__sun

		__GLIBC__
	));
	use constant bad_macros  => {
		"__sgi__" => "__sgi",
		"__sparc__" => "__sparc",
		"__sparc_v9__" => "__sparcv9",
		"__sun__" => "__sun",
		"__svr4__" => "__SVR4",
	};

	$rest = $text;
	while ($rest =~ s/defined\((__[\w_]+)\)// || $rest =~ s/\b(_\w+)\(//) {
		my ($macro) = ($1);

		if (exists(good_macros->{$macro})) {
			$opt_debug_misc and $line->log_debug("Found good macro \"${macro}\".");
		} elsif (exists(bad_macros->{$macro})) {
			$line->log_warning("The macro \"${macro}\" is not portable enough. Please use \"".bad_macros->{$macro}."\" instead.");
			$line->explain_warning("See the pkgsrc guide, section \"CPP defines\" for details.");

		} elsif ($macro eq "__NetBSD_Prereq__") {
			$line->log_warning("Please use __NetBSD_Version__ instead of __NetBSD_Prereq__.");
			$line->explain_warning(
"The __NetBSD_Prereq__ macro is pretty new. It was born in NetBSD",
"4.99.3, and maybe it won't survive for long. A better (and compatible)",
"way is to compare __NetBSD_Version__ directly to the required version",
"number.");

		} elsif ($macro =~ m"^_+NetBSD_+Version_+$"i && $macro ne "__NetBSD_Version__") {
			$line->log_warning("Misspelled variant \"${macro}\" of \"__NetBSD_Version__\".");

		} else {
			$opt_debug_unchecked and $line->log_debug("Unchecked macro \"${macro}\".");
		}
	}
}

# Checks whether the line contains text that looks like absolute
# pathnames, assuming that the file uses the common syntax with
# single or double quotes to represent strings.
#
sub checkline_source_absolute_pathname($$) {
	my ($line, $text) = @_;
	my ($abspath);

	$opt_debug_trace and $line->log_debug("checkline_source_absolute_pathname(${text})");

	if ($text =~ m"(.*)([\"'])(/[^\"']*)\2") {
		my ($before, $delim, $string) = ($1, $2, $3);

		$opt_debug_misc and $line->log_debug("checkline_source_absolute_pathname(before=${before}, string=${string})");
		if ($before =~ m"[A-Z_]+\s*$") {
			# allowed: PREFIX "/bin/foo"

		} elsif ($string =~ m"^/[*/]") {
			# This is more likely to be a C or C++ comment.

		} elsif ($string !~ m"^/\w") {
			# Assume that pathnames start with a letter or digit.

		} elsif ($before =~ m"\+\s*$") {
			# Something like foodir + '/lib'

		} else {
			$abspath = $string;
		}
	}

	if (defined($abspath)) {
		checkword_absolute_pathname($line, $abspath);
	}
}

# Last resort if the file does not look like a Makefile or typical
# source code. All strings that look like pathnames and start with
# one of the typical Unix prefixes are found.
#
sub checkline_other_absolute_pathname($$) {
	my ($line, $text) = @_;

	$opt_debug_trace and $line->log_debug("checkline_other_absolute_pathname(\"${text}\")");

	if ($text =~ m"^#[^!]") {
		# Don't warn for absolute pathnames in comments,
		# except for shell interpreters.

	} elsif ($text =~ m"^(.*?)((?:/[\w.]+)*/(?:bin|dev|etc|home|lib|mnt|opt|proc|sbin|tmp|usr|var)\b[\w./\-]*)(.*)$") {
		my ($before, $path, $after) = ($1, $2, $3);

		if ($before =~ m"\@$") {
			# Something like @PREFIX@/bin

		} elsif ($before =~ m"[)}]$") {
			# Something like ${prefix}/bin or $(PREFIX)/bin

		} elsif ($before =~ m"\+\s*[\"']$") {
			# Something like foodir + '/lib'

		} elsif ($before =~ m"\w$") {
			# Something like $dir/lib

		} elsif ($before =~ m"\.$") {
			# ../foo is not an absolute pathname.

		} else {
			$opt_debug_misc and $line->log_debug("before=${before}");
			checkword_absolute_pathname($line, $path);
		}
	}
}

sub checkfile_patch($) {
	my ($fname) = @_;
	my ($lines);
	my ($state, $redostate, $nextstate, $dellines, $addlines, $hunks);
	my ($seen_comment, $current_fname, $current_ftype, $patched_files);
	my ($leading_context_lines, $trailing_context_lines, $context_scanning_leading);

	# Abbreviations used:
	# style: [c] = context diff, [u] = unified diff
	# scope: [f] = file, [h] = hunk, [l] = line
	# action: [d] = delete, [m] = modify, [a] = add, [c] = context
	use constant re_patch_rcsid	=> qr"^\$.*\$$";
	use constant re_patch_text	=> qr"^(.+)$";
	use constant re_patch_empty	=> qr"^$";
	use constant re_patch_cfd	=> qr"^\*\*\*\s(\S+)(.*)$";
	use constant re_patch_cfa	=> qr"^---\s(\S+)(.*)$";
	use constant re_patch_ch	=> qr"^\*{15}(.*)$";
	use constant re_patch_chd	=> qr"^\*{3}\s(\d+)(?:,(\d+))?\s\*{4}$";
	use constant re_patch_cha	=> qr"^-{3}\s(\d+)(?:,(\d+))?\s-{4}$";
	use constant re_patch_cld	=> qr"^(?:-\s(.*))?$";
	use constant re_patch_clm	=> qr"^(?:!\s(.*))?$";
	use constant re_patch_cla	=> qr"^(?:\+\s(.*))?$";
	use constant re_patch_clc	=> qr"^(?:\s\s(.*))?$";
	use constant re_patch_ufd	=> qr"^---\s(\S+)(?:\s+(.*))?$";
	use constant re_patch_ufa	=> qr"^\+{3}\s(\S+)(?:\s+(.*))?$";
	use constant re_patch_uh	=> qr"^\@\@\s-(?:(\d+),)?(\d+)\s\+(?:(\d+),)?(\d+)\s\@\@(.*)$";
	use constant re_patch_uld	=> qr"^-(.*)$";
	use constant re_patch_ula	=> qr"^\+(.*)$";
	use constant re_patch_ulc	=> qr"^\s(.*)$";
	use constant re_patch_ulnonl	=> qr"^\\ No newline at end of file$";

	use enum qw(:PST_
		START CENTER TEXT
		CFA CH CHD CLD0 CLD CLA0 CLA
		UFA UH UL
	);

	my @comment_explanation = (
"Each patch must document why it is necessary. If it has been applied",
"because of a security issue, a reference to the CVE should be mentioned",
"as well.",
"",
"Since it is our goal to have as few patches as possible, all patches",
"should be sent to the upstream maintainers of the package. After you",
"have done so, you should add a reference to the bug report containing",
"the patch.");

	my ($line, $m);

	my $check_text = sub($) {
		my ($text) = @_;

		if ($text =~ m"(\$(Author|Date|Header|Id|Locker|Log|Name|RCSfile|Revision|Source|State|$opt_rcsidstring)(?::[^\$]*)?\$)") {
			my ($tag) = ($2);

			if ($text =~ re_patch_uh) {
				$line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it.");
				$line->set_text($1);
			} else {
				$line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".");
			}
		}
	};

	my $check_contents = sub() {

		if ($m->has(1)) {
			$check_text->($m->text(1));
		}
	};

	my $check_added_contents = sub() {
		my $text;

		return unless $m->has(1);
		$text = $m->text(1);
		checkline_cpp_macro_names($line, $text);

		# XXX: This check is not as accurate as the similar one in
		# checkline_mk_shelltext().
		if (defined($current_fname)) {
			if ($current_ftype eq "shell" || $current_ftype eq "make") {
				my ($mm, $rest) = match_all($text, $regex_shellword);

				foreach my $m (@{$mm}) {
					my $shellword = $m->text(1);

					if ($shellword =~ m"^#") {
						last;
					}
					checkline_mk_absolute_pathname($line, $shellword);
				}

			} elsif ($current_ftype eq "source") {
				checkline_source_absolute_pathname($line, $text);

			} elsif ($current_ftype eq "configure") {
				if ($text =~ m": Avoid regenerating within pkgsrc$") {
					$line->log_error("This code must not be included in patches.");
					$line->explain_error(
"It is generated automatically by pkgsrc after the patch phase.",
"",
"For more details, look for \"configure-scripts-override\" in",
"mk/configure/gnu-configure.mk.");
				}

			} elsif ($current_ftype eq "ignore") {
				# Ignore it.

			} else {
				checkline_other_absolute_pathname($line, $text);
			}
		}
	};

	my $check_hunk_end = sub($$$) {
		my ($deldelta, $adddelta, $newstate) = @_;

		if ($deldelta > 0 && $dellines == 0) {
			$redostate = $newstate;
			if (defined($addlines) && $addlines > 0) {
				$line->log_error("Expected ${addlines} more lines to be added.");
			}
		} elsif ($adddelta > 0 && $addlines == 0) {
			$redostate = $newstate;
			if (defined($dellines) && $dellines > 0) {
				$line->log_error("Expected ${dellines} more lines to be deleted.");
			}
		} else {
			if (defined($context_scanning_leading)) {
				if ($deldelta != 0 && $adddelta != 0) {
					if ($context_scanning_leading) {
						$leading_context_lines++;
					} else {
						$trailing_context_lines++;
					}
				} else {
					if ($context_scanning_leading) {
						$context_scanning_leading = false;
					} else {
						$trailing_context_lines = 0;
					}
				}
			}

			if ($deldelta != 0) {
				$dellines -= $deldelta;
			}
			if ($adddelta != 0) {
				$addlines -= $adddelta;
			}
			if (!((defined($dellines) && $dellines > 0) ||
			      (defined($addlines) && $addlines > 0))) {
				if (defined($context_scanning_leading)) {
					if ($leading_context_lines != $trailing_context_lines) {
						$opt_debug_patches and $line->log_warning("The hunk that ends here does not have as many leading (${leading_context_lines}) as trailing (${trailing_context_lines}) lines of context.");
					}
				}
				$nextstate = $newstate;
			}
		}
	};

	# @param deldelta
	#	The number of lines that are deleted from the patched file.
	# @param adddelta
	#	The number of lines that are added to the patched file.
	# @param newstate
	#	The follow-up state when this line is the last line to be
	#	added in this hunk of the patch.
	#
	my $check_hunk_line = sub($$$) {
		my ($deldelta, $adddelta, $newstate) = @_;

		$check_contents->();
		$check_hunk_end->($deldelta, $adddelta, $newstate);

		# If -Wextra is given, the context lines are checked for
		# absolute paths and similar things. If it is not given,
		# only those lines that really add something to the patched
		# file are checked.
		if ($adddelta != 0 && ($deldelta == 0 || $opt_warn_extra)) {
			$check_added_contents->();
		}
	};

	# [ regex, to state, action ]
	my $transitions = {
		PST_START() =>
		[   [re_patch_rcsid, PST_CENTER, sub() {
			checkline_rcsid($line, "");
		}], [undef, PST_CENTER, sub() {
			checkline_rcsid($line, "");
		}]],
		PST_CENTER() =>
		[   [re_patch_empty, PST_TEXT, sub() {
			#
		}], [re_patch_cfd, PST_CFA, sub() {
			if ($seen_comment) {
				$opt_warn_space and $line->log_note("Empty line expected.");
			} else {
				$line->log_error("Comment expected.");
				$line->explain_error(@comment_explanation);
			}
			$line->log_warning("Please use unified diffs (diff -u) for patches.");
		}], [re_patch_ufd, PST_UFA, sub() {
			if ($seen_comment) {
				$opt_warn_space and $line->log_note("Empty line expected.");
			} else {
				$line->log_error("Comment expected.");
				$line->explain_error(@comment_explanation);
			}
		}], [undef, PST_TEXT, sub() {
			$opt_warn_space and $line->log_note("Empty line expected.");
		}]],
		PST_TEXT() =>
		[   [re_patch_cfd, PST_CFA, sub() {
			if (!$seen_comment) {
				$line->log_error("Comment expected.");
				$line->explain_error(@comment_explanation);
			}
			$line->log_warning("Please use unified diffs (diff -u) for patches.");
		}], [re_patch_ufd, PST_UFA, sub() {
			if (!$seen_comment) {
				$line->log_error("Comment expected.");
				$line->explain_error(@comment_explanation);
			}
		}], [re_patch_text, PST_TEXT, sub() {
			$seen_comment = true;
		}], [re_patch_empty, PST_TEXT, sub() {
			#
		}], [undef, PST_TEXT, sub() {
			#
		}]],
		PST_CFA() =>
		[   [re_patch_cfa, PST_CH, sub() {
			$current_fname = $m->text(1);
			$current_ftype = get_filetype($line, $current_fname);
			$opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype");
			$patched_files++;
			$hunks = 0;
		}]],
		PST_CH() =>
		[   [re_patch_ch, PST_CHD, sub() {
			$hunks++;
		}]],
		PST_CHD() =>
		[   [re_patch_chd, PST_CLD0, sub() {
			$dellines = ($m->has(2))
			    ? (1 + $m->text(2) - $m->text(1))
			    : ($m->text(1));
		}]],
		PST_CLD0() =>
		[   [re_patch_clc, PST_CLD, sub() {
			$check_hunk_line->(1, 0, PST_CLD0);
		}], [re_patch_cld, PST_CLD, sub() {
			$check_hunk_line->(1, 0, PST_CLD0);
		}], [re_patch_clm, PST_CLD, sub() {
			$check_hunk_line->(1, 0, PST_CLD0);
		}], [re_patch_cha, PST_CLA0, sub() {
			$dellines = undef;
			$addlines = ($m->has(2))
			    ? (1 + $m->text(2) - $m->text(1))
			    : ($m->text(1));
		}]],
		PST_CLD() =>
		[   [re_patch_clc, PST_CLD, sub() {
			$check_hunk_line->(1, 0, PST_CLD0);
		}], [re_patch_cld, PST_CLD, sub() {
			$check_hunk_line->(1, 0, PST_CLD0);
		}], [re_patch_clm, PST_CLD, sub() {
			$check_hunk_line->(1, 0, PST_CLD0);
		}], [undef, PST_CLD0, sub() {
			if ($dellines != 0) {
				$line->log_warning("Invalid number of deleted lines (${dellines} missing).");
			}
		}]],
		PST_CLA0() =>
		[   [re_patch_clc, PST_CLA, sub() {
			$check_hunk_line->(0, 1, PST_CH);
		}], [re_patch_clm, PST_CLA, sub() {
			$check_hunk_line->(0, 1, PST_CH);
		}], [re_patch_cla, PST_CLA, sub() {
			$check_hunk_line->(0, 1, PST_CH);
		}], [undef, PST_CH, sub() {
			#
		}]],
		PST_CLA() =>
		[   [re_patch_clc, PST_CLA, sub() {
			$check_hunk_line->(0, 1, PST_CH);
		}], [re_patch_clm, PST_CLA, sub() {
			$check_hunk_line->(0, 1, PST_CH);
		}], [re_patch_cla, PST_CLA, sub() {
			$check_hunk_line->(0, 1, PST_CH);
		}], [undef, PST_CLA0, sub() {
			if ($addlines != 0) {
				$line->log_warning("Invalid number of added lines (${addlines} missing).");
			}
		}]],
		PST_CH() =>
		[   [undef, PST_TEXT, sub() {
			#
		}]],
		PST_UFA() =>
		[   [re_patch_ufa, PST_UH, sub() {
			$current_fname = $m->text(1);
			$current_ftype = get_filetype($line, $current_fname);
			$opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype");
			$patched_files++;
			$hunks = 0;
		}]],
		PST_UH() =>
		[   [re_patch_uh, PST_UL, sub() {
			$dellines = ($m->has(1) ? $m->text(2) : 1);
			$addlines = ($m->has(3) ? $m->text(4) : 1);
			$check_text->($line->text);
			if ($line->text =~ m"\r$") {
				$line->log_error("The hunk header must not end with a CR character.");
				$line->explain_error(
"The MacOS X patch utility cannot handle these.");
			}
			$hunks++;
			$context_scanning_leading = (($m->has(1) && $m->text(1) ne "1") ? true : undef);
			$leading_context_lines = 0;
			$trailing_context_lines = 0;
		}], [undef, PST_TEXT, sub() {
			($hunks != 0) || $line->log_warning("No hunks for file ${current_fname}.");
		}]],
		PST_UL() =>
		[   [re_patch_uld, PST_UL, sub() {
			$check_hunk_line->(1, 0, PST_UH);
		}], [re_patch_ula, PST_UL, sub() {
			$check_hunk_line->(0, 1, PST_UH);
		}], [re_patch_ulc, PST_UL, sub() {
			$check_hunk_line->(1, 1, PST_UH);
		}], [re_patch_ulnonl, PST_UL, sub() {
			#
		}], [re_patch_empty, PST_UL, sub() {
			$opt_warn_space and $line->log_note("Leading white-space missing in hunk.");
			$check_hunk_line->(1, 1, PST_UH);
		}], [undef, PST_UH, sub() {
			if ($dellines != 0 || $addlines != 0) {
				$line->log_warning("Unexpected end of hunk (-${dellines},+${addlines} expected).");
			}
		}]]};

	$opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_patch()");

	checkperms($fname);
	if (!($lines = load_lines($fname, false))) {
		log_error($fname, NO_LINE_NUMBER, "Could not be read.");
		return;
	}
	if (@{$lines} == 0) {
		log_error($fname, NO_LINE_NUMBER, "Must not be empty.");
		return;
	}

	$state = PST_START;
	$dellines = undef;
	$addlines = undef;
	$patched_files = 0;
	$seen_comment = false;
	$current_fname = undef;
	$current_ftype = undef;
	$hunks = undef;

	for (my $lineno = 0; $lineno <= $#{$lines}; ) {
		$line = $lines->[$lineno];
		my $text = $line->text;

		$opt_debug_patches and $line->log_debug("[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."] $text");

		my $found = false;
		foreach my $t (@{$transitions->{$state}}) {
				if (!defined($t->[0])) {
					$m = undef;
				} elsif ($text =~ $t->[0]) {
					$opt_debug_patches and $line->log_debug($t->[0]);
					$m = PkgLint::SimpleMatch->new($text, \@-, \@+);
				} else {
					next;
				}
				$redostate = undef;
				$nextstate = $t->[1];
				$t->[2]->();
				if (defined($redostate)) {
					$state = $redostate;
				} else {
					$state = $nextstate;
					if (defined($t->[0])) {
						$lineno++;
					}
				}
				$found = true;
				last;
		}

		if (!$found) {
			$line->log_error("Parse error: state=${state}");
			$state = PST_TEXT;
			$lineno++;
		}
	}

	while ($state != PST_TEXT) {
		$opt_debug_patches and log_debug($fname, "EOF", "[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."]");

		my $found = false;
		foreach my $t (@{$transitions->{$state}}) {
			if (!defined($t->[0])) {
				my $newstate;

				$m = undef;
				$redostate = undef;
				$nextstate = $t->[1];
				$t->[2]->();
				$newstate = (defined($redostate)) ? $redostate : $nextstate;
				if ($newstate == $state) {
					log_fatal($fname, "EOF", "Internal error in the patch transition table.");
				}
				$state = $newstate;
				$found = true;
				last;
			}
		}

		if (!$found) {
			log_error($fname, "EOF", "Parse error: state=${state}");
			$state = PST_TEXT;
		}
	}

	if ($patched_files > 1) {
		log_warning($fname, NO_LINE_NUMBER, "Contains patches for $patched_files files, should be only one.");

	} elsif ($patched_files == 0) {
		log_error($fname, NO_LINE_NUMBER, "Contains no patch.");
	}

	checklines_trailing_empty_lines($lines);
}