tar: don't use "((" in shell scripts
[debian/tar] / scripts / tar-snapshot-edit
1 #! /usr/bin/perl -w
2 # Display and edit the 'dev' field in tar's snapshots
3 # Copyright (C) 2007 Free Software Foundation, Inc.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3, or (at your option)
8 # any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 # 02110-1301, USA.
19 #
20 # Author: Dustin J. Mitchell <dustin@zmanda.com>
21 #
22 # This script is capable of replacing values in the 'dev' field of an
23 # incremental backup 'snapshot' file.  This is useful when the device
24 # used to store files in a tar archive changes, without the files
25 # themselves changing.  This may happen when, for example, a device
26 # driver changes major or minor numbers.
27
28 use Getopt::Std;
29
30 ## reading
31
32 sub read_incr_db ($) {
33     my $filename = shift;
34     open(my $file, "<$filename") || die "Could not open '$filename' for reading";
35
36     my $header_str = <$file>;
37     my $file_version;
38     if ($header_str =~ /^GNU tar-[^-]*-([0-9]+)\n$/) {
39         $file_version = $1+0;
40     } else {
41         $file_version = 0;
42     }
43
44     print "file version $file_version\n";
45
46     if ($file_version == 0) {
47         return read_incr_db_0($file, $header_str);
48     } elsif ($file_version == 1) {
49         return read_incr_db_1($file);
50     } elsif ($file_version == 2) {
51         return read_incr_db_2($file);
52     } else {
53         die "Unrecognized snapshot version in header '$header_str'";
54     }
55 }
56
57 sub read_incr_db_0 ($$) {
58     my $file = shift;
59     my $header_str = shift;
60
61     my $hdr_timestamp_sec = $header_str;
62     chop $hdr_timestamp_sec;
63     my $hdr_timestamp_nsec = ''; # not present in file format 0
64
65     my @dirs;
66
67     while (<$file>) {
68         /^([0-9]*) ([0-9]*) (.*)\n$/ || die("Bad snapshot line $_");
69
70         push @dirs, { dev=>$1,
71                       ino=>$2,
72                       name=>$3 };
73     }
74
75     close($file);
76
77     # file version, timestamp, timestamp, dir list
78     return [ 0, $hdr_timestamp_sec, $hdr_timestamp_nsec, \@dirs ];
79 }
80
81 sub read_incr_db_1 ($) {
82     my $file = shift;
83
84     my $timestamp = <$file>; # "sec nsec"
85     my ($hdr_timestamp_sec, $hdr_timestamp_nsec) = ($timestamp =~ /([0-9]*) ([0-9]*)/);
86
87     my @dirs;
88
89     while (<$file>) {
90         /^([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*) (.*)\n$/ || die("Bad snapshot line $_");
91
92         push @dirs, { timestamp_sec=>$1,
93                       timestamp_nsec=>$2,
94                       dev=>$3,
95                       ino=>$4,
96                       name=>$5 };
97     }
98
99     close($file);
100
101     # file version, timestamp, timestamp, dir list
102     return [ 1, $hdr_timestamp_sec, $hdr_timestamp_nsec, \@dirs ];
103 }
104
105 sub read_incr_db_2 ($) {
106     my $file = shift;
107
108     $/="\0"; # $INPUT_RECORD_SEPARATOR
109     my $hdr_timestamp_sec = <$file>;
110     chop $hdr_timestamp_sec;
111     my $hdr_timestamp_nsec = <$file>;
112     chop $hdr_timestamp_nsec;
113     my @dirs;
114
115     while (1) {
116         last if eof($file);
117
118         my $nfs = <$file>;
119         my $timestamp_sec = <$file>;
120         my $timestamp_nsec = <$file>;
121         my $dev = <$file>;
122         my $ino = <$file>;
123         my $name = <$file>;
124
125         # get rid of trailing NULs
126         chop $nfs;
127         chop $timestamp_sec;
128         chop $timestamp_nsec;
129         chop $dev;
130         chop $ino;
131         chop $name;
132
133         my @dirents;
134         while (my $dirent = <$file>) {
135             chop $dirent;
136             push @dirents, $dirent;
137             last if ($dirent eq "");
138         }
139         die "missing terminator" unless (<$file> eq "\0");
140
141         push @dirs, { nfs=>$nfs,
142                       timestamp_sec=>$timestamp_sec,
143                       timestamp_nsec=>$timestamp_nsec,
144                       dev=>$dev,
145                       ino=>$ino,
146                       name=>$name,
147                       dirents=>\@dirents };
148     }
149
150     close($file);
151     $/ = "\n"; # reset to normal
152
153     # file version, timestamp, timestamp, dir list
154     return [ 2, $hdr_timestamp_sec, $hdr_timestamp_nsec, \@dirs ];
155 }
156
157 ## display
158
159 sub show_device_counts ($$) {
160     my $info = shift;
161     my $filename = shift;
162     my %devices;
163     foreach my $dir (@{${@$info}[3]}) {
164         my $dev = ${%$dir}{'dev'};
165         $devices{$dev}++;
166     }
167
168     foreach $dev (sort keys %devices) {
169         printf "$filename: Device 0x%04x occurs $devices{$dev} times.\n", $dev;
170     }
171 }
172
173 ## editing
174
175 sub replace_device_number ($@) {
176     my $info = shift(@_);
177     my @repl = @_;
178
179     foreach my $dir (@{${@$info}[3]}) {
180         foreach $x (@repl) {
181             if (${%$dir}{'dev'} eq $$x[0]) {
182                 ${%$dir}{'dev'} = $$x[1];
183                 last;
184             }
185         }
186     }
187 }
188
189 ## writing
190
191 sub write_incr_db ($$) {
192     my $info = shift;
193     my $filename = shift;
194     my $file_version = $$info[0];
195
196     open($file, ">$filename") || die "Could not open '$filename' for writing";
197
198     if ($file_version == 0) {
199         write_incr_db_0($info, $file);
200     } elsif ($file_version == 1) {
201         write_incr_db_1($info, $file);
202     } elsif ($file_version == 2) {
203         write_incr_db_2($info, $file);
204     } else {
205         die "Unknown file version $file_version.";
206     }
207
208     close($file);
209 }
210
211 sub write_incr_db_0 ($$) {
212     my $info = shift;
213     my $file = shift;
214
215     my $timestamp_sec = $info->[1];
216     print $file "$timestamp_sec\n";
217
218     foreach my $dir (@{${@$info}[3]}) {
219         print $file "${%$dir}{'dev'} ";
220         print $file "${%$dir}{'ino'} ";
221         print $file "${%$dir}{'name'}\n";
222     }
223 }
224
225
226 sub write_incr_db_1 ($$) {
227     my $info = shift;
228     my $file = shift;
229
230     print $file "GNU tar-1.15-1\n";
231
232     my $timestamp_sec = $info->[1];
233     my $timestamp_nsec = $info->[2];
234     print $file "$timestamp_sec $timestamp_nsec\n";
235
236     foreach my $dir (@{${@$info}[3]}) {
237         print $file "${%$dir}{'timestamp_sec'} ";
238         print $file "${%$dir}{'timestamp_nsec'} ";
239         print $file "${%$dir}{'dev'} ";
240         print $file "${%$dir}{'ino'} ";
241         print $file "${%$dir}{'name'}\n";
242     }
243 }
244
245
246 sub write_incr_db_2 ($$) {
247     my $info = shift;
248     my $file = shift;
249
250     print $file "GNU tar-1.16-2\n";
251
252     my $timestamp_sec = $info->[1];
253     my $timestamp_nsec = $info->[2];
254     print $file $timestamp_sec . "\0";
255     print $file $timestamp_nsec . "\0";
256
257     foreach my $dir (@{${@$info}[3]}) {
258         print $file ${%$dir}{'nfs'} . "\0";
259         print $file ${%$dir}{'timestamp_sec'} . "\0";
260         print $file ${%$dir}{'timestamp_nsec'} . "\0";
261         print $file ${%$dir}{'dev'} . "\0";
262         print $file ${%$dir}{'ino'} . "\0";
263         print $file ${%$dir}{'name'} . "\0";
264         foreach my $dirent (@{${%$dir}{'dirents'}}) {
265             print $file $dirent . "\0";
266         }
267         print $file "\0";
268     }
269 }
270
271 ## main
272
273 sub main {
274     our ($opt_b, $opt_r, $opt_h);
275     getopts('br:h');
276     HELP_MESSAGE() if ($opt_h || $#ARGV == -1 || ($opt_b && !$opt_r));
277
278     my @repl;
279     if ($opt_r) {
280         foreach my $spec (split(/,/, $opt_r)) {
281             ($spec =~ /^([^-]+)-([^-]+)/) || die "Invalid replacement specification '$opt_r'";
282             push @repl, [interpret_dev($1), interpret_dev($2)];
283         }
284     }
285
286     foreach my $snapfile (@ARGV) {
287         my $info = read_incr_db($snapfile);
288         if ($opt_r ) {
289             if ($opt_b) {
290                 rename($snapfile, $snapfile . "~") || die "Could not rename '$snapfile' to backup";
291             }
292
293             replace_device_number($info, @repl);
294             write_incr_db($info, $snapfile);
295         } else {
296             show_device_counts($info, $snapfile);
297         }
298     }
299 }
300
301 sub HELP_MESSAGE {
302     print "Usage: tar-snapshot-edit.pl [-r 'DEV1-DEV2[,DEV3-DEV4...]' [-b]] SNAPFILE [SNAPFILE [..]]\n";
303     print "\n";
304     print "  Without -r, summarize the 'device' values in each SNAPFILE.\n";
305     print "\n";
306     print "  With -r, replace occurrences of DEV1 with DEV2 in each SNAPFILE.\n";
307     print "  DEV1 and DEV2 may be specified in hex (e.g., 0xfe01), decimal (e.g.,\n";
308     print "  65025), or MAJ:MIN (e.g., 254:1).  To replace multiple occurrences,\n";
309     print "  separate them with commas.  If -b is also specified, backup\n";
310     print "  files (ending with '~') will be created.\n";
311     exit 1;
312 }
313
314 sub interpret_dev ($) {
315     my $dev = shift;
316
317     if ($dev =~ /^([0-9]+):([0-9]+)$/) {
318         return $1 * 256 + $2;
319     } elsif ($dev =~ /^0x[0-9a-fA-F]+$/) {
320         return oct $dev;
321     } elsif ($dev =~ /^[0-9]+$/) {
322         return $dev+0;
323     } else {
324         die "Invalid device specification '$dev'";
325     }
326 }
327
328 main