[comp.sys.ti.explorer] File recovery tool

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))))