iOS7のユーザ辞書をリセットするスクリプト

前回までに自分のiPhoneMacBookのユーザ辞書は同期するようになったのだけど、その手順はちょっと複雑だった。再び同期できなくなった時のことを考えて、素早く、安全に、iPhoneのユーザ辞書をリセットするスクリプトにしておこうと、思い立った。

開発&実行環境

mbdbを編集するRubyコード(reset_keyboard.rb)

  • いちいちテキストに書き出さずとも、mbdbの内容を取捨選択できるように改良した。
  • 「mbdb.reject!(/HomeDomain::Library\/Keyboard/)」によって、キーボードに関連する設定ファイルを一括削除している。
# encoding: ASCII-8BIT

class Mbdb
  def initialize(mbdb_filename, verbose = false)
    @verbose = verbose
    process_mbdb_file(mbdb_filename)
  end

  def to_text_file(filename)
    File.open(filename, 'w') do |f|
      @mbdb.each do |r|
        f.puts r.values.inspect
      end
    end
  end

  def to_mbdb_file(filename)
    File.open(filename, 'wb') do |f|
      f.write(binary_data)
    end
  end

  def reject!(regexp)
    @mbdb.reject! {|r| "#{r[:domain]}::#{r[:filename]}" =~ regexp}
  end

  def fill!(regexp)
    @mbdb.reject! {|r| !("#{r[:domain]}::#{r[:filename]}" =~ regexp)}
  end

private

  # Return an integer (big-endian) and new offset from the current offset
  def get_int(data, offset, intsize)
    value = 0
    while intsize > 0
      value = (value<<8) + data[offset].ord
      offset += 1
      intsize -= 1
    end
    return value, offset
  end

  # Return a string and new offset from the current offset into the data
  def get_string(data, offset)
    return 'ffff', offset + 2 if data[offset] == 0xFF.chr and data[offset + 1] == 0xFF.chr # Blank string
    length, offset = get_int(data, offset, 2) # 2-byte length
    value = data[offset...(offset + length)]
    value = '0000' if value == ""
    return value, (offset + length)
  end

  def put_int(data_10, intsize)
    data_16 = "%0#{intsize*2}x" % data_10
    [data_16].pack('H*')
  end

  def put_string(str)
    return "\xff\xff" if str == 'ffff'
    return "\x00\x00" if str == '0000'
    return [str.length].pack('n') + str
  end

  def process_mbdb_file(filename)
    @mbdb = Array.new
    data = File.open(filename, 'rb') { |f| f.read }
    puts "MBDB file read. Size: #{data.size}"
    raise 'This does not look like an MBDB file' if data[0...4] != 'mbdb'
    offset = 4
    offset += 2 # value x05 x00, not sure what this is
    while offset < data.size
      fileinfo = {}
      fileinfo[:domain], offset = get_string(data, offset)
      fileinfo[:filename], offset = get_string(data, offset)
      fileinfo[:linktarget], offset = get_string(data, offset)
      fileinfo[:datahash], offset = get_string(data, offset)
      fileinfo[:unknown1], offset = get_string(data, offset)
      fileinfo[:mode], offset = get_int(data, offset, 2)
      fileinfo[:unknown2], offset = get_int(data, offset, 4)
      fileinfo[:unknown3], offset = get_int(data, offset, 4)
      fileinfo[:userid], offset = get_int(data, offset, 4)
      fileinfo[:groupid], offset = get_int(data, offset, 4)
      fileinfo[:mtime], offset = get_int(data, offset, 4)
      fileinfo[:atime], offset = get_int(data, offset, 4)
      fileinfo[:ctime], offset = get_int(data, offset, 4)
      fileinfo[:filelen], offset = get_int(data, offset, 8)
      fileinfo[:flag], offset = get_int(data, offset, 1)
      fileinfo[:propertynum], offset = get_int(data, offset, 1)
      fileinfo[:properties] = {}
      (0...(fileinfo[:propertynum])).each do |i|
        propname, offset = get_string(data, offset)
        propval, offset = get_string(data, offset)
        fileinfo[:properties][propname] = propval
      end
      @mbdb << fileinfo
    end
    @mbdb
  end

  def binary_data
    bin = "mbdb\x05\x00"
    @mbdb.each do |h|
      bin << put_string(h[:domain])
      bin << put_string(h[:filename])
      bin << put_string(h[:linktarget])
      bin << put_string(h[:datahash])
      bin << put_string(h[:unknown1])
      bin << put_int(h[:mode], 2)
      bin << put_int(h[:unknown2], 4)
      bin << put_int(h[:unknown3], 4)
      bin << put_int(h[:userid], 4)
      bin << put_int(h[:groupid], 4)
      bin << put_int(h[:mtime], 4)
      bin << put_int(h[:atime], 4)
      bin << put_int(h[:ctime], 4)
      bin << put_int(h[:filelen], 8)
      bin << put_int(h[:flag], 1)
      bin << put_int(h[:propertynum], 1)
      h[:properties].each do |k, v|
        bin << put_string(k)
        bin << put_string(v)
      end
    end
    bin
  end

end

if RUBY_VERSION >= "1.9" then
  `mv '#{ARGV[0]}' '#{ARGV[0]}.back'`
  mbdb = Mbdb.new("#{ARGV[0]}.back", true)
  mbdb.reject!(/HomeDomain::Library\/Keyboard/) # Reset UserDictionary
  mbdb.to_text_file("#{ARGV[0]}.txt")
  mbdb.to_mbdb_file(ARGV[0])
else
  puts 'Needs Ruby version 1.9 or later'
end

バックアップを保護するシェルスクリプト(reset_keyboard.sh)

  • 上記Rubyコードがmbdbを修正する前に、バックアップをコピーして、安全に作業する環境を整える。
#!/bin/sh
current_dir=`dirname $0`
target_dir="$1"
reset_dir="${target_dir}-reset-user-dictionary"

rm -fr "$reset_dir";
cp -r "$target_dir" "$reset_dir"

display_name=`defaults read "${reset_dir}/Info.plist" "Display Name"`
defaults write "${reset_dir}/Info.plist" "Display Name" -string "${display_name}-reset-user-dictionary"

ruby "${current_dir}/reset_keyboard.rb" "${reset_dir}/Manifest.mbdb"

バックアップリストを返すシェルスクリプト(current_backup_list.sh)

  • iPhoneのバックアップは、端末固有のUniqueDeviceIDという英数字が羅列したフォルダに存在する。
  • リセットする端末を指定する時、英数字の羅列では扱いにくいので、端末名を関連づけたリストを返す。
#!/bin/sh
backup_folders=`find "$HOME/Library/Application Support/MobileSync/Backup" -name Info.plist -exec dirname {} \;`
IFS=$'\n'
for f in $backup_folders
do
  deviceID=`defaults read "$f/Manifest.plist" 'Lockdown'|grep UniqueDeviceID|awk '{print $3}'|tr -d ';'`
  folder_name=`basename "$f"`
  if [ "$deviceID" = "$folder_name" ]; then
    echo "`defaults read "$f/Info.plist" 'Display Name'` :: $folder_name"
  fi
done

AppleScriptアプリケーションによるGUI(reset_iOS7_UserDictionary.app)

  • 以上のスクリプトを、AppleScriptアプリケーションとしてまとめてみた。
  • iOS端末を指定すると、上記スクリプトが連携して、ユーザ辞書をリセットしたバックアップを生成するのだ。


activate
set current_backup_list_sh to (path to resource "current_backup_list.sh")'s POSIX path
set reset_keyboard_sh to (path to resource "reset_keyboard.sh")'s POSIX path

set device_list to (do shell script current_backup_list_sh)'s paragraphs
set selected_item to choose from list device_list with prompt "ユーザ辞書をリセットする端末を選択してください。" with title my name
if selected_item is false then error number -128 --キャンセル
set selected_folder to split(selected_item as text, " :: ")'s item 2

display notification "ユーザ辞書をリセット中です..."
delay 1

set mobilesync_backup to (((path to application support folder from user domain) as text) & "MobileSync:Backup:")'s POSIX path
do shell script (reset_keyboard_sh & space & quoted form of (mobilesync_backup & selected_folder)) -- & " >& /dev/null &"

display notification "リセットが完了しました。"
delay 1




on split(src_text, delimiter) set last_delimiter to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set res to src_text's text items
set AppleScript's text item delimiters to last_delimiter
res
end split

ダウンロードと使い方


例:MacBookiPhoneのユーザ辞書をリセットする場合

  • すべての端末(MacBookiPhone)で、iCloudの「書類とデータ」の同期をオフにする。
iCloudのユーザ辞書のリセット
  • WebブラウザiCloudにアクセスして、「書類とデータのリセット」を実行する。
MacBookのユーザ辞書のリセット
  • MacBookで、~/Library/Mobile Documents/com~apple~TextInput/ を削除する。(Finderでゴミ箱へ)
iPhoneのユーザ辞書のリセット
  • iTunesを起動して「今すぐバックアップ」を実行する。
    • 事前に、写真や動画をiPhotoなどに取り込んで、カメラロールを空っぽにしておくと、バックアップが素早く完了する。
  • reset_iOS7_UserDictionary.appを実行する。
    • ユーザ辞書の設定がリセットされたバックアップを生成する。

  • iTunesで、「バックアップを復元...」を実行する。
  • バックアップリストから「端末名-reset-user-dictionary」を選択して、復元ボタンを押す。

  • 復元が成功すれば、iPhoneのユーザ辞書はリセットされているはず。
  • すべての端末(MacBookiPhone)で、iCloudの「書類とデータ」の同期をオンに戻す。

待つこと暫し、ユーザ辞書の同期が始まるかもしれない。

  • 不要になった「端末名-reset-user-dictionary」は、iTunes >> 環境設定... >> デバイスから削除できる。