Skip to content

Instantly share code, notes, and snippets.

@snipsnipsnip
Last active August 24, 2019 16:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save snipsnipsnip/818260 to your computer and use it in GitHub Desktop.
Save snipsnipsnip/818260 to your computer and use it in GitHub Desktop.
ThumbsRB: list and extract thumbnails in Thumbs.db
require 'optparse'
require 'fileutils'
require 'iconv'
require 'ole/storage'
# parsing code is taken from lf.win.shell.thumbsdb
# http://libforensics.googlecode.com/
module ThumbsRB
def self.main
verbose = nil
extract_all = nil
output_dir = '.'
ARGV.options do |o|
o.banner = "Usage: #{File.basename $0} [options] thumbs.db [file-to-extract ..]"
o.separator "Options:"
o.on('-a', '--extract-all') { extract_all = true }
o.on('-d DIR', '--output-dir=DIR') {|d| output_dir = d }
o.on('-h', '--help') { abort o.to_s }
ARGV.parse!
abort o.to_s if ARGV.empty?
end
db, *names = ARGV
db = File.join(db, "Thumbs.db") if File.directory?(db)
File.exist?(db) or abort "no such file: '#{db}'"
Thumbs.open(db) do |thumbs|
if names.empty? && !extract_all
puts thumbs.map {|e| e.name }.sort
return
end
names = names.map {|n| n.downcase }
FileUtils.mkdir_p(output_dir)
thumbs.each do |e|
next if !extract_all && !names.include?(e.name.downcase)
path = File.join(output_dir, "#{e.name}.jpg")
STDERR.print path if $DEBUG
if File.exist?(path)
warn "'#{path}' already exists. skipping"
next
end
open(path, "wb") do |f|
f << e.data
end
warn "." if $DEBUG
end
end
end
Thumb = Struct.new(:size, :id, :mtime, :name, :data_promise)
class Thumb
def data
if data_promise.respond_to?(:call)
self.data_promise = data_promise.call
else
data_promise
end
end
end
class Thumbs
def self.open(path)
Ole::Storage.open(path) do |db|
yield new(db)
end
end
include Enumerable
def initialize(db)
@db = db
@entries = load_entries
end
def size
@entries.size
end
def each(&blk)
@entries.each(&blk)
end
private
def load_entries
@db.file.open("Catalog") do |f|
f.seek 4
size = f.read(4).unpack("V")[0]
warn "#{size} items" if $DEBUG
f.seek 16
Array.new(size) { make_thumb(f) }
end
end
def make_thumb(f)
size, id, mtime = f.read(16).unpack("V3")
rest = size - 16
if rest > 0
name = f.read(rest)
name = Iconv.iconv('utf-8', 'utf-16le', name)[0]
name.rstrip!
end
warn "##{id}: mtime #{mtime}, #{size} bytes, '#{name}'" if $DEBUG
data_promise = proc do
filename = id.to_s.reverse # WTF
data = @db.file.read(filename)
jpeg_header = "\xff\xd8"
if data[12, 2] == jpeg_header
data[12..-1]
else
warn "#{name}: no header found" unless data[16, 2] == jpeg_header if $DEBUG
data[16..-1]
end
end
Thumb.new(size, id, mtime, name, data_promise)
end
end
end
ThumbsRB.main if __FILE__ == $0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment