initial commit
[mapsector.git] / mapsector.sh
1 #!/bin/sh
2
3 ###########################################################################
4
5 # Utilities
6
7 deviceid()
8 {
9     ls -lL "$1" 2>/dev/null | awk -F '[ ,]+' '{print "(" $5 ", " $6 ")"}'
10 }
11
12 dmcryptmap()
13 {
14     devnums="`deviceid $1`"
15
16     dmsetup ls --target crypt | awk '{print $1}' | while read dmdev; do
17         if dmsetup deps $dmdev | grep -qF "$devnums"; then
18             echo "/dev/mapper/$dmdev"
19         fi
20     done
21 }
22
23 ###########################################################################
24
25 # Map device/sector through blockdevice mappings (e.g. lvm)
26
27 map()
28 {
29     while true; do
30         if detect_raid; then
31             echo "# $device: Raid detected" 1>&2
32             map_raid
33         elif detect_partition; then
34             echo "# $device: partition table detected" 1>&2
35             map_partition
36         elif detect_crypt; then
37             echo "# $device: LUKS/cryptsetup detected" 1>&2
38             map_crypt
39         elif detect_lvm; then
40             echo "# $device: LVM detected" 1>&2
41             map_lvm
42         else
43             break
44         fi
45     done
46 }
47
48 ########################################
49 #### Partitions
50
51 detect_partition()
52 {
53     [ -z "`fdisk -l $device 2>&1 >/dev/null`" ]
54 }
55
56 map_partition()
57 {
58     # Step 1: Find partition to which this sector belongs
59
60     local partdev
61     local partstart
62     local partsector
63
64     partdev="`fdisk -ul $device | awk -v sector="$sector" -F '[ *]+' '/^\// && !/Extended$/ && $2<=sector && $3>=sector {print $1,$2}'`" #`"
65
66     if [ -z "$partdev" ]; then
67         echo "# sector $sector is not part of any partition on $device" 1>&2
68         exit 0
69     fi
70
71     partstart="${partdev#* }"
72     partdev="${partdev% *}"
73     partsector="`dc -e "$sector $partstart - p"`" # `"
74
75     echo "offset $partstart"
76     echo "device $partdev partition"
77     echo "sector $partsector"
78
79     device="$partdev"
80     sector="$partsector"
81 }
82
83 ########################################
84 #### LUKS / cryptsetup
85
86 detect_crypt()
87 {
88     which cryptsetup >/dev/null 2>&1 && [ -n "`dmcryptmap $device`" ]
89 }
90
91 map_crypt()
92 {
93     # Step 2: Find the crypted volume defined for this partition
94
95     local offset
96     local devnums
97     local cryptdev
98     local cryptsector
99     local type
100
101     offset="`cryptsetup luksDump $device 2>/dev/null | awk '/^Payload offset/{print $3}'`"
102
103     if [ -z "$offset" ]; then
104         # Plain dmcrypt
105         offset=0
106         type=dmcrypt
107     else
108         type=luks
109     fi
110
111     cryptdev="`dmcryptmap $device`"
112
113     if [ -z "$cryptdev" ]; then
114         echo "! Failed to find decrypted mapper device for $device"
115         exit 1;
116     fi
117
118     cryptsector="`dc -e "$sector $offset - p"`" #`"
119
120     echo "offset $offset"
121     echo "device $cryptdev crypt"
122     echo "type $type"
123     echo "sector $cryptsector"
124     
125     device="$cryptdev"
126     sector="$cryptsector"
127 }
128
129 ########################################
130 #### LVM
131
132 detect_lvm()
133 {
134     which pvdisplay >/dev/null 2>&1 && pvdisplay $device >/dev/null 2>&1
135 }
136
137 map_lvm()
138 {
139     local pvname
140     local pesize
141     local vgname
142     local pestart
143     local penum
144     local subsector
145     local lestart
146     local firstpe
147     local lenum
148     local fsdev
149     local fssector
150     
151     # Step 3: Get pysical extent number
152
153     pvname="`pvdisplay -c $device 2>/dev/null | awk -F: '{print $1,$2,$8}' | sed -e 's/^ *//'`"
154
155     if [ -z "$pvname" ]; then
156         echo "! $device is not a physical volume" 1>&2
157         exit 1
158     fi
159
160     pesize="${pvname##* }"
161     vgname="${pvname% *}"
162     pvname="${vgname% *}"
163     vgname="${vgname#* }"
164     pesize="`dc -e "$pesize 2 * p"`" #`"
165     pestart="`pvs --unit s -ope_start $device 2>/dev/null | sed -n -e 's/ *//g' -e 's/S.*$//' -e '$p'`"
166     penum="`dc -e "$sector $pestart - $pesize ~ n [ ] n p"`" #`"
167     subsector="${penum% *}"
168     penum="${penum#* }"
169
170     echo "device $pvname pv"
171     echo "offset $pestart"
172     echo "pesize $pesize"
173     echo "extent $penum"
174     echo "subsector $subsector"
175     echo "group $vgname"
176
177     # Step 4: Find associated logical volume
178
179     lestart="$(vgdisplay -v $vgname 2>/dev/null | awk '/LV Name/{print $3}' | while read lvname; do \
180         lvdisplay -m $lvname 2>/dev/null \
181             | awk -v RS="\n *\n(  --- Segments ---\n)?" \
182                   -F"[ \t\n:]+" \
183                   -v pvname="$pvname" \
184                   -v penum="$penum" \
185                   -v lvname="$lvname" \
186                   '$0 ~ "Physical volume[ \t]+" pvname && $14<=penum && $16>=penum{print lvname,$4,$14}'; \
187         done)"
188
189     if [ -z "$lestart" ]; then
190         echo "# pysical extent $penum of $pvname is not mapped in any logical volume" 1>&2
191         exit 0
192     fi
193
194     lvname="${lestart%% *}"
195     lestart="${lestart#* }"
196     firstpe="${lestart#* }"
197     lestart="${lestart% *}"
198     lenum="`dc -e "$penum $firstpe - $lestart + p"`" #`"
199
200     echo "device $lvname lv"
201     echo "extent $lenum"
202
203     fsdev="$lvname"
204     fssector="`dc -e "$lenum $pesize * $subsector + p"`" #`"
205
206     echo "sector $fssector"
207
208     device="$fsdev"
209     sector="$fssector"
210 }
211
212 ########################################
213 #### Raid-1
214
215 detect_raid()
216 {
217     which mdadm >/dev/null 2>&1 && mdadm -Q $device | grep -qF -- "--examine"
218 }
219
220 map_raid()
221 {
222     local mddevice
223     local mdlevel
224
225     mddevice="`mdadm -Q $device | sed -ne 's/.*\(raid[0-9] \/dev\/[^.]*\).*/\1/' -eT -ep`"
226
227     if [ -z "$mddevice" ]; then
228         echo "! raid master device for raid componentn device $device not found" 1>&2
229         exit 1
230     fi
231
232     mdlevel="${mddevice% *}"
233     mddevice="${mddevice#* }"
234     
235     echo "device $mddevice md"
236     echo "raidlevel $mdlevel"
237     echo "sector $sector"
238
239     device="$mddevice"
240 }
241
242 ###########################################################################
243
244 # Scan the final blockdevice for additional information.
245 # Information includes mountpoint, filesystem type and filesystem type
246 # specific information: filesysetm block, inode number and filename
247
248 scan()
249 {
250     scan_mountpoint
251     if detect_ext2fs; then
252         echo "# $device: ext2/3/4 filesystem detected" 1>&2
253         scan_ext2fs
254     elif detect_reiserfs; then
255         echo "# $device: reiserfs filesystem detected" 1>&2
256         scan_reiserfs
257     fi
258 }
259
260 ########################################
261 #### Mountpoint
262
263 scan_mountpoint()
264 {
265     local devnums
266     devnums="`deviceid $device`"
267
268     # Step 5: Find filesystem mount point
269
270     while read dev dir opts; do
271         case "$dev" in
272             *:*) ;;
273             *)
274                 if [ "$devnums" == "`deviceid $dev`" ]; then
275                     echo "mountpoint $dir"
276                     break
277                 fi
278                 ;;
279         esac
280     done < /proc/mounts
281 }
282
283 ########################################
284 #### ext2/ext3
285
286 detect_ext2fs()
287 {
288     which tune2fs >/dev/null 2>&1 && tune2fs -l $device >/dev/null 2>&1
289 }
290
291 scan_ext2fs()
292 {
293     local fsblocksize
294     local fsblock
295     local fssubsector
296     local fstype
297     local inode
298
299     fstype="`file -s $device | sed -e 's/.*\(ext[0-9]\).*/\1/'`"
300     echo "fstype $fstype"
301
302     # Step 6: Get filesystem blocksize and convert sector number to filesystem block number
303
304     fsblocksize="`tune2fs -l $device | awk '/Block size/{print $3/512}'`"
305
306     if [ -z "$fsblocksize" ]; then
307         echo "! $device is not ext2/ext3" 1>&2
308         exit 1
309     fi
310
311     fsblock="`dc -e "$sector $fsblocksize ~ n [ ] n p"`" #`"
312     fssubsector="${fsblock% *}"
313     fsblock="${fsblock#* }"
314
315     echo "blocksize $fsblocksize"
316     echo "block $fsblock"
317     echo "subsector $fssubsector"
318
319     # Step 7: Check, whether block is in use
320
321     if echo "testb $fsblock" | debugfs $device 2>/dev/null | grep -qF "not in use"; then
322         echo "blockstate free"
323         exit 0
324     fi
325     echo "blockstate used"
326
327     # Step 8: Find inode, to which the block belongs
328
329     inode="`echo "icheck $fsblock" | debugfs $device 2>/dev/null | awk 'FNR>1{print $2}'`" #`"
330
331     if [ -z "$inode" ]; then
332         echo "blocktype meta?"
333         exit 0
334     fi
335
336     echo "inode $inode"
337
338     # Step 9: Find file name(s) referencing the inode
339
340     (
341         namefound="$(\
342             echo "ncheck $inode" \
343                 | debugfs $device 2>/dev/null \
344                 | sed -e '1d' -e 's/^[0-9]*[    ]*//' -e 's/^\/\//\//' \
345                 | while read name; do \
346                 if [ -z "$firstname" ]; then \
347                     echo "blocktype data" 1>&3; \
348                     echo "1"; \
349                     firstname=1; \
350                 fi; \
351                 echo "name $name" 1>&3; \
352             done \
353         )"
354         if [ -z "$namefound" ]; then
355             echo "blocktype journal?"
356         fi
357     ) 3>&1
358 }
359
360 ########################################
361 #### Reiser-FS
362
363 detect_reiserfs()
364 {
365     which debugreiserfs >/dev/null 2>&1 && debugreiserfs $device >/dev/null 2>&1
366 }
367
368 scan_reiserfs()
369 {
370     local blocksize
371     local block
372     local subsector
373
374     echo "fstype reiserfs"
375
376     # Step 6: Get filesystem blocksize and convert sector number to filesystem block number
377
378     blocksize="`debugreiserfs $device 2>/dev/null | awk '/^Blocksize:/{print $2/512}'`"
379     
380     if [ -z "$blocksize" ]; then
381         echo "! $device is not reiserfs" 1>&2
382         exit 1
383     fi
384
385     block="`dc -e "$sector $blocksize ~ n [ ] n p"`" #`"
386     subsector="${block% *}"
387     block="${block#* }"
388
389     echo "blocksize $blocksize"
390     echo "block $block"
391     echo "subsector $subsector"
392
393     # Step 7: Check, whether block is in use
394
395     if debugreiserfs -1 $block $device 2>&1 >/dev/null | grep -qF "free in ondisk bitmap"; then
396         echo "blockstate free"
397         exit 0
398     fi
399     echo "blockstate used"
400
401     # Use debugreiserfs -1 to check the block type. This however only works if the block is readable.
402     type="`debugreiserfs -1 $block $device 2>/dev/null | sed -e '/^=*$/d' | head -1`"
403
404     case "$type" in
405         "Looks like unformatted")  type="data"          ;;
406         "Reiserfs super block"*)   type="superblock"    ;;
407         "LEAF NODE"*)              type="meta"          ;;
408         "INTERNAL NODE"*)          type="meta"          ;;
409         "Desc block"*)             type="journal"       ;;
410         *)                         type=""              ;;
411     esac
412
413     if [ -n "$type" ]; then
414         echo "blocktype $type"
415     fi
416
417     # Step 8: Find object id to which this block belongs
418     # Step 9: Find file name(s) referencing this object id
419
420     # Currently we only look for $block in indirect blocks.
421
422     python - $device $block <<EOF
423 import sys
424 import os
425 import os.path
426
427 device = sys.argv[1]
428 blocknr = int(sys.argv[2])
429
430 dirtree = {}
431 blockid = None
432
433 fp = os.popen("debugreiserfs -d %s 2>/dev/null" % device)
434
435 def parse_leafnode():
436     global fp
437     l = fp.readline()
438     #sys.stderr.write("> leafnode(1): %s\n" % repr(l))
439     if not l.startswith("LEAF NODE") : return
440     for i in range(4) : fp.readline()
441     while True:
442         l = fp.readline()
443         #sys.stderr.write("> leafnode(2): %s\n" % repr(l))
444         parts = l.split("|")
445         if len(parts)<2 : return
446         parts = parts[2].split()
447         if len(parts)<4 : return
448         obid=(int(parts[0]),int(parts[1]))
449         if   parts[3] == "DIR" : parse_dir(obid)
450         elif parts[3] == "IND" : parse_indirect(obid)
451         else:
452             while True:
453                 l =  fp.readline().strip()
454                 #sys.stderr.write("> leafnode(3): %s\n" % repr(l))
455                 if l == 79*"-" or l == 67*"=" : break
456
457 def parse_dir(obid):
458     global fp
459     global dirtree
460     fp.readline()
461     while True:
462         try:
463             l = fp.readline().rstrip()
464             #sys.stderr.write("> dir: %s\n" % repr(l))
465             if l == 79*"-" or l == 67*"=": return
466             if l.endswith("not set"): continue
467             i = l.index("\"(")
468             name = l[6:6+int(l[i+2:i+5])]
469             if name == "." or name == ".." : continue
470             i = l.index("[",len(name)+12)
471             entryid = tuple(map(int,l[i+1:l.index("]",i+1)].split()))
472             entry = dirtree.get(entryid)
473             if entry is None : entry = dirtree[entryid] = set()
474             entry.add((name,obid))
475         except ValueError:
476             pass
477
478 def parse_indirect(obid):
479     global fp
480     global blocknr
481     global blockid
482     fp.readline()
483     for pointer in fp.readline().strip()[1:-1].split():
484         i = pointer.find("(")
485         if i == -1:
486             blkmin = blkmax = int(pointer)
487         else:
488             blkmin = int(pointer[:i])
489             blkmax = blkmin+int(pointer[i+1:-1])-1
490         if blocknr >= blkmin and blocknr <= blkmax:
491             blockid = obid
492     fp.readline()
493
494 def scan_path(id):
495     global dirtree
496     entry = dirtree.get(id)
497     #sys.stderr.write("> scan_path: %s->%s\n" % (id, entry))
498     if entry is None:
499         return [ '/' ]
500     else:
501         rv = []
502         for name, parent in entry:
503             for path in scan_path(parent):
504                 rv.append(os.path.join(path,name))
505         return rv
506
507 def parse():
508     global fp
509     while True:
510         while True:
511             l = fp.readline()
512             #sys.stderr.write("> parse: %s\n" % repr(l))
513             if not(l) : return
514             if l.strip() == 67*"=" : break
515         parse_leafnode()
516
517 parse()
518
519 if blockid is None:
520     sys.exit(0)
521
522 sys.stdout.write("objectid %d %d\n" % blockid)
523
524 for path in scan_path(blockid):
525     if path != '/':
526         sys.stdout.write("name %s\n" % path)
527 EOF
528
529 }
530
531 ###########################################################################
532
533 unset LANG
534
535 noscan=""
536 if [ "$1" == "--noscan" ]; then
537     noscan=1
538     shift
539 fi
540
541 if [ -z "$2" ]; then
542     cat <<EOF
543 Usage: $0 [--noscan] <device> <sector>"
544
545 mapsector -- Map sector numbers to file name(s)
546
547 Given a device and a sector number, mapsector will try to find the
548 file name(s) mapping to this sector. It will try to gather as much
549 information about the given sector as possible.
550
551 mapsector currently has support for the following mapping schemes:
552
553    partition table
554    RAID-1
555    cryptsetup (luks and plain dmcrypt)
556    LVM in linear allocation mode
557
558 mapsector currently supports the following filesystems
559
560    ext2/3/4
561    reiserfs
562
563 mapsector will try it's best to find an associated file name but
564 depending on the filesystem state and the type of sector (e.g. if
565 the sector is part of a filesystem metadata block) this may not be
566 possible.
567
568 For mapsector to work, the filesystem must be currently active
569 (e.g. LVM must be running, crypted devices must have been set up). The
570 filesystem must not necessarily be mounted (though if mounted,
571 mapsector will give you the mountpoint).
572
573 if '--noscan' is given, the possibly lengthy (!!) filesystem scan for
574 filenames is skipped.
575 EOF
576     exit 1
577 fi
578
579 device="$1"
580 sector="$2"
581
582 echo "device $device"
583 echo "sector $sector"
584
585 map
586 if [ -z "$noscan" ]; then
587     scan
588 fi