1# 2010 January 7
2#
3# The author disclaims copyright to this source code.  In place of
4# a legal notice, here is a blessing:
5#
6#    May you do good and not evil.
7#    May you find forgiveness for yourself and forgive others.
8#    May you share freely, never taking more than you give.
9#
10#***********************************************************************
11# This file implements utility functions for SQLite library.
12#
13# This file attempts to restore the header of a journal.
14# This may be useful for rolling-back the last committed
15# transaction from a recovered journal.
16#
17
18package require sqlite3
19
20set parm_error 0
21set fix_chksums 0
22set dump_pages 0
23set db_name ""
24
25for {set i 0} {$i<$argc} {incr i} {
26  if {[lindex $argv $i] == "-fix_chksums"} {
27    set fix_chksums -1
28  } elseif {[lindex $argv $i] == "-dump_pages"} {
29    set dump_pages -1
30  } elseif {$db_name == ""} {
31    set db_name [lindex $argv $i]
32    set jrnl_name $db_name-journal
33  } else {
34    set parm_error -1
35  }
36}
37if {$parm_error || $db_name == ""} {
38  puts "USAGE: restore_jrnl.tcl \[-fix_chksums\] \[-dump_pages\] db_name"
39  puts "Example: restore_jrnl.tcl foo.sqlite"
40  return
41}
42
43# is there a way to determine this?
44set sectsz 512
45
46# Copy file $from into $to
47#
48proc copy_file {from to} {
49  file copy -force $from $to
50}
51
52# Execute some SQL
53#
54proc catchsql {sql} {
55  set rc [catch {uplevel [list db eval $sql]} msg]
56  list $rc $msg
57}
58
59# Perform a test
60#
61proc do_test {name cmd expected} {
62  puts -nonewline "$name ..."
63  set res [uplevel $cmd]
64  if {$res eq $expected} {
65    puts Ok
66  } else {
67    puts Error
68    puts "  Got: $res"
69    puts "  Expected: $expected"
70  }
71}
72
73# Calc checksum nonce from journal page data.
74#
75proc calc_nonce {jrnl_pgno} {
76  global sectsz
77  global db_pgsz
78  global jrnl_name
79  set jrnl_pg_offset [expr $sectsz+((4+$db_pgsz+4)*$jrnl_pgno)]
80  set nonce [hexio_get_int [hexio_read $jrnl_name [expr $jrnl_pg_offset+4+$db_pgsz] 4]]
81  for {set i [expr $db_pgsz-200]} {$i>0} {set i [expr $i-200]} {
82    set byte [hexio_get_int [hexio_read $jrnl_name [expr $jrnl_pg_offset+4+$i] 1]]
83    set nonce [expr $nonce-$byte]
84  }
85  return $nonce
86}
87
88# Calc checksum from journal page data.
89#
90proc calc_chksum {jrnl_pgno} {
91  global sectsz
92  global db_pgsz
93  global jrnl_name
94  global nonce
95  set jrnl_pg_offset [expr $sectsz+((4+$db_pgsz+4)*$jrnl_pgno)]
96  set chksum $nonce
97  for {set i [expr $db_pgsz-200]} {$i>0} {set i [expr $i-200]} {
98    set byte [hexio_get_int [hexio_read $jrnl_name [expr $jrnl_pg_offset+4+$i] 1]]
99    set chksum [expr $chksum+$byte]
100  }
101  return $chksum
102}
103
104# Print journal page data in hex dump form
105#
106proc dump_jrnl_page {jrnl_pgno} {
107  global sectsz
108  global db_pgsz
109  global jrnl_name
110
111  # print a header block for the page
112  puts [string repeat "-" 79]
113  set jrnl_pg_offset [expr $sectsz+((4+$db_pgsz+4)*$jrnl_pgno)]
114  set db_pgno [hexio_get_int [hexio_read $jrnl_name [expr $jrnl_pg_offset] 4]]
115  set chksum [hexio_get_int [hexio_read $jrnl_name [expr $jrnl_pg_offset+4+$db_pgsz] 4]]
116  set nonce [calc_nonce $jrnl_pgno]
117  puts [ format {jrnl_pg_offset: %08x (%d)  jrnl_pgno: %d  db_pgno: %d} \
118      $jrnl_pg_offset $jrnl_pg_offset \
119      $jrnl_pgno $db_pgno]
120  puts [ format {nonce: %08x chksum: %08x} \
121      $nonce $chksum]
122
123  # now hex dump the data
124  # This is derived from the Tcler's WIKI
125  set fid [open $jrnl_name r]
126  fconfigure $fid -translation binary -encoding binary
127  seek $fid [expr $jrnl_pg_offset+4]
128  set data [read $fid $db_pgsz]
129  close $fid
130  for {set addr 0} {$addr<$db_pgsz} {set addr [expr $addr+16]} {
131    # get 16 bytes of data
132    set s [string range $data $addr [expr $addr+16]]
133
134    # Convert the data to hex and to characters.
135    binary scan $s H*@0a* hex ascii
136
137    # Replace non-printing characters in the data.
138    regsub -all -- {[^[:graph:] ]} $ascii {.} ascii
139
140    # Split the 16 bytes into two 8-byte chunks
141    regexp -- {(.{16})(.{0,16})} $hex -> hex1 hex2
142
143    # Convert the hex to pairs of hex digits
144    regsub -all -- {..} $hex1 {& } hex1
145    regsub -all -- {..} $hex2 {& } hex2
146
147    # Print the hex and ascii data
148    puts [ format {%08x %-24s %-24s %-16s} \
149        $addr $hex1 $hex2 $ascii ]
150  }
151}
152
153# Setup for the tests.  Make a backup copy of the files.
154#
155if [file exist $db_name.org] {
156  puts "ERROR: during back-up: $db_name.org exists already."
157  return;
158}
159if [file exist $jrnl_name.org] {
160  puts "ERROR: during back-up: $jrnl_name.org exists already."
161  return
162}
163copy_file $db_name $db_name.org
164copy_file $jrnl_name $jrnl_name.org
165
166set db_fsize [file size $db_name]
167set db_pgsz [hexio_get_int [hexio_read $db_name 16 2]]
168set db_npage [expr {$db_fsize / $db_pgsz}]
169
170set jrnl_fsize [file size $jrnl_name]
171set jrnl_npage [expr {($jrnl_fsize - $sectsz) / (4 + $db_pgsz + 4)}]
172
173# calculate checksum nonce for first page
174set nonce [calc_nonce 0]
175
176# verify all the pages in the journal use the same nonce
177for {set i 1} {$i<$jrnl_npage} {incr i} {
178  set tnonce [calc_nonce $i]
179  if {$tnonce != $nonce} {
180    puts "WARNING: different nonces: 0=$nonce $i=$tnonce"
181    if {$fix_chksums } {
182      set jrnl_pg_offset [expr $sectsz+((4+$db_pgsz+4)*$i)]
183      set tchksum [calc_chksum $i]
184      hexio_write $jrnl_name [expr $jrnl_pg_offset+4+$db_pgsz] [format %08x $tchksum]
185      puts "INFO: fixing chksum: $i=$tchksum"
186    }
187  }
188}
189
190# verify all the page numbers in the journal
191for {set i 0} {$i<$jrnl_npage} {incr i} {
192  set jrnl_pg_offset [expr $sectsz+((4+$db_pgsz+4)*$i)]
193  set db_pgno [hexio_get_int [hexio_read $jrnl_name $jrnl_pg_offset 4]]
194  if {$db_pgno < 1} {
195    puts "WARNING: page number < 1: $i=$db_pgno"
196  }
197  if {$db_pgno >= $db_npage} {
198    puts "WARNING: page number >= $db_npage: $i=$db_pgno"
199  }
200}
201
202# dump page data
203if {$dump_pages} {
204  for {set i 0} {$i<$jrnl_npage} {incr i} {
205    dump_jrnl_page $i
206  }
207}
208
209# write the 8 byte magic string
210hexio_write $jrnl_name 0 d9d505f920a163d7
211
212# write -1 for number of records
213hexio_write $jrnl_name 8 ffffffff
214
215# write 00 for checksum nonce
216hexio_write $jrnl_name 12 [format %08x $nonce]
217
218# write page count
219hexio_write $jrnl_name 16 [format %08x $db_npage]
220
221# write sector size
222hexio_write $jrnl_name 20 [format %08x $sectsz]
223
224# write page size
225hexio_write $jrnl_name 24 [format %08x $db_pgsz]
226
227# check the integrity of the database with the patched journal
228sqlite3 db $db_name
229do_test restore_jrnl-1.0 {
230  catchsql {PRAGMA integrity_check}
231} {0 ok}
232db close
233
234