acuff@SUMEX-AIM.STANFORD.EDU (Richard Acuff) (06/27/91)
I recently had occasion to attempt the recovery of some files that had been deleted and expunged from an Explorer file system. In the process, I hacked together the following file of code to help do this. This might save you the trouble of having to dissect the disk representation of the Explorer file system, and is pretty small, so here it is... -- Rich ;;; -*- Mode:Common-Lisp; Package:FILE-SYSTEM; Base:10 -*- ;;; ********************************************************************** ;;; Copyright (c) 1991 Stanford University. ;;; Copyright is held by Stanford University except where code has been ;;; modified from TI source code. In these cases TI code is marked with ;;; a suitable comment. ;;; All Stanford Copyright code is in the public domain. This code may be ;;; distributed and used without restriction as long as this copyright ;;; notice is included and no fee is charged. ;;; TI source code may only be distributed to users who hold valid TI ;;; software licenses. ;;; ********************************************************************** ;;; This software developed by: ;;; Rich Acuff ;;; at the Stanford University Knowledge Systems Lab in Jun '91. ;;; ;;; This work was supported in part by: ;;; NIH Grant 5 P41 RR00785-15 ;;; This file contains tools for attempting to restore accidentally ;;; deleted files in the Explorer file system. ;;; The main entry is RECOVER-GHOST-FILES. ;;;---------------------------------------------------------------------- ;;; This stuff is for stream access to the raw data in a partition. (defparameter PARTITION-STREAM-N-BLOCKS-AT-A-TIME 100 "Number of blocks to read from the disk at one time in a PARTITION-STREAM.") (defflavor PARTITION-STREAM (unit ;Disk unit of our partition partition-name ;Name of our partition end-idx ;1+ last block page-idx ;Current block part-base ;Starting block buffer ;8bit byte buffer n-pages-at-a-time ;How many pages to read at once rqb ;RQB used to read disk show-count? ;Show count of pages? ) (sys:buffered-input-character-stream) :initable-instance-variables :gettable-instance-variables :settable-instance-variables (:documentation "Stream access to partitions. Use OPEN-PARTITION-STREAM to open.")) (defun OPEN-PARTITION-STREAM (unit partition-name) "Open an input stream that will read all the data in partition PARTITION-NAME on disk UNIT." (make-instance 'partition-stream :unit unit :partition-name partition-name :n-pages-at-a-time partition-stream-n-blocks-at-a-time :show-count? t)) (defmethod (PARTITION-STREAM :AFTER :INIT) (&rest ignore) "Initialize IV's." (declare (ignore ignore)) (multiple-value-bind (part-start length) (find-disk-partition partition-name nil unit) (setf part-base part-start rqb (fs:get-disk-rqb n-pages-at-a-time) page-idx part-start end-idx (+ part-start length) buffer (make-array (* n-pages-at-a-time page-size-in-bytes) :element-type '(mod 256))))) (defmethod (PARTITION-STREAM :READ-DISK-PAGES) (first-page) "Reads pages from disk, starting with FIRST-PAGE." (let (disk-data n-pages) ;;Don't go past end (setf n-pages (min n-pages-at-a-time (- end-idx page-idx))) (fs:disk-read rqb unit first-page n-pages) (setf disk-data (array-leader rqb 2)) ;;Convert to 8bit bytes. There's got to be a better way... (loop for i from 0 to (1- (* n-pages (/ page-size-in-bytes 2))) as n = (elt disk-data i) do (setf (elt buffer (* i 2)) (ldb #O0010 n) (elt buffer (1+ (* i 2))) (ldb #O1010 n))) ;;Returns suitable for use in :NEXT-INPUT-BUFFER (values buffer 0 (* n-pages page-size-in-bytes)))) (defmethod (PARTITION-STREAM :NEXT-INPUT-BUFFER) (&optional ignore) "Stream interface. Also does block counting." (declare (ignore ignore)) (when (and show-count? (zerop (mod (- page-idx part-base) 100))) (format t " ~D" (/ (- page-idx part-base) 100)) (when (zerop (mod (- page-idx part-base) 1000)) (format t "~%"))) (if (>= page-idx end-idx) nil ;EOF (send self :read-disk-pages page-idx))) (defmethod (PARTITION-STREAM :AFTER :CLOSE) (&rest ignore) "Returns the RQB." (declare (ignore ignore)) (fs:return-disk-rqb rqb) (setf rqb :this-stream-is-closed)) (defmethod (PARTITION-STREAM :DISCARD-INPUT-BUFFER) (&optional ignore) "Stream interface. Increment the pointer." (declare (ignore ignore)) (incf page-idx n-pages-at-a-time)) (defmethod (PARTITION-STREAM :SET-BUFFER-POINTER) (new-pointer) "Stream interface. Compute right set of pages." (let ((nth-buf (floor new-pointer (* n-pages-at-a-time page-size-in-bytes)))) (setf page-idx (+ nth-buf part-base)) ;;Byte number of first byte of pages that contain the byte NEW-POINTER. (* nth-buf (* n-pages-at-a-time page-size-in-bytes)))) ;;;---------------------------------------------------------------------- ;;;Routines to look for and try to parse directory entries (defun TRY-TO-READ-DIRECTORY-ENTRY (stream name type) "NAME and TYPE are the file name and type of a directory entry. They've just been read from STREAM. Try to parse the rest of the directory entry and return a file object. Print an error message and return NIL if the parse fails. If there is an error but the map read, returns a file anyway. The resulting file object should only be used to copy out data since several fields will be wrong." (let (version default-byte-size author creation-date map (attributes 0) (properties nil)) (catch-error (setf version (get-bytes stream 3.) default-byte-size (get-bytes stream 1.) author (send stream :line-in t) creation-date (get-bytes stream 4.) map (map-read stream) attributes (get-bytes stream 2.) properties (read-property-list stream))) ;;If we were able to read the map, we might be able to salvage ;;something... (when map (make-file name name displacement 0 ;unknown entry-length 0 ;unknown type type version version default-byte-size default-byte-size files :disk open-count 0. author-internal author creation-date-internal creation-date directory (dc-root-directory) ;unknown map map attributes attributes plist properties)))) (defun SCAN-FOR-GHOST-FILES (name type &optional (unit 1) (part "FILE")) "Scans partition PART on unit UNIT for data on the disk that might be a directory entry for a file described by NAME and TYPE. Returns a FS:FILE structure for each possible match. Prints status info to *STANDARD-OUTPUT*." (let ((name-length (length name)) (files nil) file read-pointer) (with-open-stream (s (open-partition-stream unit part)) (loop as line = (read-line s nil :eof) until (eq line :eof) do (when (string-equal name line ;;The file name comes at end of line :start2 (- (length line) name-length)) ;;Name matches - test for type (takes up whole line) (setf line (read-line s nil :eof)) (when (string-equal type line) ;;Remember this in case we get an error. (setf read-pointer (send s :read-pointer)) (format t "~&[Match at offset ~D..." (- read-pointer name-length (length type) 1)) (setf file (try-to-read-directory-entry s name type)) (if file (progn (push file files) (format t "Version ~D]~%" (file-version file))) (progn ;;Since error, reset pointer (send s :set-pointer read-pointer) (format t "couldn't parse directory]~%"))))))) files)) (defun ATTEMPT-TO-RECOVER-GHOST-FILE (file to-dir) "FILE is an FS:FILE object. Use it's map to copy out data to a new file named Vversion-name.type in the directory TO-DIR. VERSION, NAME, and TYPE are fields of FILE." (let ((out-path (make-pathname :defaults to-dir :name (format nil "V~D-~A" (file-version file) (file-name file)) :type (file-type file)))) (with-open-file (out out-path :direction :output) (with-map-stream-in (in (file-map file)) (sys:stream-copy-until-eof in out))))) (defun RECOVER-GHOST-FILES (name type copy-to-dir &optional (unit 1) (part "FILE")) "This function scans the disk partition PART on unit UNIT, looking for occurences of NAME and TYPE that might indicate a directory entry for a file. It tries to copy any such file to COPY-TO-DIR. A log of possible matches, what disk page they are on, and the results of trying to recover that file is written to *STANDARD-OUTPUT*. The partition doesn't have to be booted for the search, but will have to be for the actual attempt to recover data. The purpose is to recover incorrectly deleted files. To be successful, the directory entry must not have been written over and the pages of the deleted and expunged file must not have been reused as part of another file. This does not currently work for files that have directory entries that are split across pages such that they are non-contiguous. It also might not work well with multi-partition file systems (ie. VBATs)." (let ((possibles (scan-for-ghost-files name type unit part))) (loop for possible in possibles do (attempt-to-recover-ghost-file possible copy-to-dir))))