Vim editing a binary plist file

Using macOS, you may have had experiences of handling plist files. For example, ~/Library/Preferences/.GlobalPreferences.plist file holds some configurations of macOS. When you type defaults write -g ApplePressAndHoldEnabled -bool false on terminal, the following lines are added to .GlobalPreferences.plist:

<key>ApplePressAndHoldEnabled</key>
<false/>

So when you dig down the preferences or resources of macOS system, you’ll meet plist files.

vim-plist

darfink’s vim-plist plugin handles *.plist files quite well. A plist file is in one of three formats; json, binary, xml. macOS is bundled with the plutil command that can convert a plist file from one format to another. The plugin also uses plutil to handle read and write of plist files.

The plugin registers autocmd for BufReadCmd and FileReadCmd to read *.plist files, BufWriteCmd and FileWriteCmd to write *.plist files. BufRead and BufWrite events are triggered after reading the file into the buffer, but BufReadCmd and BufWriteCmd events are triggered before reading the file, and that autocmd should handle actual read and write operation of that file. These differences make handling plist files more complex.

Problems

*.strings files

Overall, the plugin is quite useful and seamless. But recently, I found some *.strings plist files under /System/Library, almost of them in binary formats. The plugin registers autocmds only for *.plist files, so there is no chance to convert them to readable formats.

At first, I’ve considered to register an autocmd for *.strings file, but I’m not sure about that .strings extension is only used for plist file, and also there can be other extensions with plist contents (for example, *.nib files are plist, too).

Saving a new file

darfink’s vim-plist checks g:plist_save_format and b:plist_save_format before writing to plist files. The buffer-local variable is set when the plugin reads the file and detect the format. The global one is set by user, and overrides buffer-local one.

I don’t want the plugin to override the content of plist files with different format, so I haven’t set g:plist_save_format. Then the problem raised. Open a new plist file, like vim test.plist, and save it after editing. Then the plugin didn’t set b:plist_save_format because it’s a new file, and I also didn’t set g:plist_save_format, so the plugin don’t know the format to use for saving.

I think this problem can be solved by patching the plugin, but its last commit is pushed in 2014, which makes me use the faster way.

Solving the problems with .vimrc

Binary plist files

I decided to check if the file is binary plist file or not, and load with functions of vim-plist plugin. The checking process is easy, because the file starts with bplist. At first, I think it’s okay to register an autocmd for BufReadCmd because calling functions of the plugin should be easy. So my first try was:

function! s:DetectBinaryPlist()
  let l:filename = expand('<afile>')
  if filereadable(l:filename)
    let l:content = readfile(l:filename, 'b', 1)
    if len(content) > 0 && content[0] =~# '^bplist'
      return 1
    endif
  endif
  return 0
endfunction
autocmd BufReadCmd *
      \ if s:DetectBinaryPlist() |
      \   call plist#Read(1) |
      \   call plist#ReadPost() |
      \ endif

Can you see the problem? This makes Vim returns an empty buffer when it reads a file that’s not a binary plist file. As I said above, BufReadCmd should handle actual read and write operation of the file. If it’s not a binary plist file, Vim won’t read anything according to this code.

So I changed BufReadCmd to BufRead. This event happens after reading the file into the buffer, so I have to empty the buffer.

function! s:ConvertBinaryPlist()
  silent! execute '%d'
  call plist#Read(1)
  call plist#ReadPost()
endfunction
autocmd BufRead *
      \ if getline(1) =~# '^bplist' |
      \   call s:ConvertBinaryPlist() |
      \ endif

The getline(1) reads the first line of the buffer, and we can call it because it’s after reading the file. It’s working quite well, so at that time, I wanted to bring the writing functionality also.

function! s:ConvertBinaryPlist()
  silent! execute '%d'
  call plist#Read(1)
  call plist#ReadPost()

  autocmd! BufWriteCmd,FileWriteCmd <buffer>
  autocmd BufWriteCmd,FileWriteCmd <buffer>
        \ call plist#Write()
endfunction

Note that the autocmd! line means deleting every other BufWriteCmd and FileWriteCmd autocmds for that buffer, and the second line registers BufWriteCmd and FileWriteCmd for that buffer.

But when I saved after editing a *.strings file, I saw this error message:

<stdin>: Property List error: Unable to convert string to correct encoding / JSON error: JSON text did not start with array or object and option to allow fragments not set.

After poking around, I found that the fileencoding is set to latin1. The original file is binary and we just replaced the contents of the buffer, the fileencoding was not properly set. So I just set it to UTF-8.

function! s:ConvertBinaryPlist()
  silent! execute '%d'
  call plist#Read(1)
  call plist#ReadPost()
  set fileencoding=utf-8

  autocmd! BufWriteCmd,FileWriteCmd <buffer>
  autocmd BufWriteCmd,FileWriteCmd <buffer>
        \ call plist#Write()
endfunction

Saving a new file

It’s simple:

autocmd BufNewFile *.plist
      \ if !get(b:, 'plist_original_format') |
      \   let b:plist_original_format = 'xml' |
      \ endif

We don’t write binary file by our own hands, so a new plist file would be in xml format.

Conclusion

So this is the complete part of my .vimrc for binary plist files:

function! s:ConvertBinaryPlist()
  silent! execute '%d'
  call plist#Read(1)
  call plist#ReadPost()
  set fileencoding=utf-8

  autocmd! BufWriteCmd,FileWriteCmd <buffer>
  autocmd BufWriteCmd,FileWriteCmd <buffer>
        \ call plist#Write()
endfunction
autocmd BufRead *
      \ if getline(1) =~# '^bplist' |
      \   call s:ConvertBinaryPlist() |
      \ endif
autocmd BufNewFile *.plist
      \ if !get(b:, 'plist_original_format') |
      \   let b:plist_original_format = 'xml' |
      \ endif

It’s also available on GitHub, you can visit my dotfiles repository!