Как преобразовать строковую кодировку Ruby в win32 и обратно

У меня есть ситуация, когда я создаю временные файлы из Ruby в папке пользователя. Я использую Dir.mktmpdir() для создания временной папки, в которую я помещаю свои файлы.

По какой-то причине эта функция дает мне короткое имя папки Windows, поэтому вместо, например. C:\Users\very long username\AppData\Local\Temp\20181018-5548 Я получаю что-то вроде C:\Users\VERYLO~1\AppData\Local\Temp\20181018-5548. Это первопричина моей проблемы, но у меня сложилось впечатление, что я не могу это исправить, потому что мне приходится работать в среде, над которой у меня нет большого контроля (Ruby 2.0 и 2.2, встроенные в SketchUp). если быть точным). Это также ограничивает количество внешних библиотек Ruby, которые я могу разумно использовать.

Чтобы получить фактическое длинное имя папки, я вызывал функцию Win32 GetLongPathName() через класс WIN32API, и это было в значительной степени успешным.

Однако у меня возникают проблемы со специальными символами. Похоже, что буферы, которые я отправляю в Win32 API (а также возвращаемые буферы), должны иметь определенную кодировку, а строки Ruby - UTF-8 (или предполагают, что возвращаемые значения будут UTF-8). Я бы с радостью способствовал любым изменениям в кодировке, но я несколько запутался в том, какую кодировку выбрать. Я даже не уверен, используется ли расширенная версия Win32 API.

Есть кое-что, что добавляет странности и заставляет меня задуматься, может быть, я лаю не по тому дереву: используя международную и/или английскую версию Windows, я могу без проблем отправлять и получать все виды специальных символов в вызове Win32 API. . Однако, как только я использую другой языковой выпуск Windows (я пробовал бразильский португальский, но у нас были люди с ивритом, и некоторые восточноевропейские выпуски Windows также сообщают об этой проблеме), он перестает работать.

def self.get_long_win32_filename(short_name)
    require 'Win32API'
    max_path = 1024
    long_name = " " * max_path
    lfn_size = Win32API.new("kernel32", "GetLongPathName", ['P','P','L'],'L').call(short_name, long_name, max_path)
    return (1..max_path).include?(lfn_size) ? long_name[0..lfn_size-1] : short_name
end 

Вот код, который я использую:

Любая помощь в выяснении того, как подойти к проблеме кодирования при передаче строк в Win32 API и из него, очень ценится!


person Timm Dapper    schedule 18.10.2018    source источник
comment
У меня нет окна под рукой, чтобы проверить, но AFAIR windows использует UTF16 для хранения имен файлов. Просто конвертируйте в UTF16, и все будет готово.   -  person Aleksei Matiushkin    schedule 18.10.2018
comment
Привет @AlekseiMatiushkin, спасибо за комментарий! Это (к сожалению) не вся история. Windows использует UTF-16, когда используется API для широких символов, но я даже не знаю, так ли это. На самом деле я подозреваю, что это не так, потому что иначе откуда взялась бы зависимость от языковой версии Windows? Кроме того, я попытался преобразовать в UTF-16, добавив строку short_name = short_name.encode(Encoding::UTF_16), и это не помогло.   -  person Timm Dapper    schedule 18.10.2018


Ответы (3)


Для работы с этими языками нужно использовать GetLongPathNameW. Единственная проблема заключается в том, что эта функция использует широкие строки, поэтому вам нужно будет вызывать функции WinAPI, которые преобразуют многобайтовые строки в широкие строки и наоборот.

В драгоценном камне windows-pr эти функции уже четко определены как multi_to_wide и wide_to_multi. Я не знаю, можете ли вы использовать драгоценные камни в своей среде, если вы не можете, попробуйте эти «упрощенные» версии:

def mb_to_wide(str)
  # CP_UTF8 = 65001
  wsize = Win32API.new("kernel32", "MultiByteToWideChar", 'ILSIPI', 'I').call(65001, 0, str, -1, nil, 0)
  if wsize > 0
    wstr = " " * wsize * 2
    Win32API.new("kernel32", "MultiByteToWideChar", 'ILSIPI', 'I').call(65001, 0, str, -1, wstr, wsize)
    wstr
  end
end

def wide_to_mb(wstr)
  wstr << "\000\000" if wstr[-1].chr != "\000" # add wide null terminators if not found
  size = Win32API.new("kernel32", "WideCharToMultiByte", 'ILSIPIPP', 'I').call(65001, 0, wstr, -1, 0, 0, nil, nil)
  if size > 0
    str = " " * wstr.length
    Win32API.new("kernel32", "WideCharToMultiByte", 'ILSIPIPP', 'I').call(65001, 0, wstr, -1, str, wstr.length, nil, nil)
    str[/^[^\0]*/] # up to \0
  end
end

Тогда вам просто нужно адаптировать свою функцию к чему-то вроде этого:

def self.get_long_win32_filename(short_name)
  max_path = 1024
  long_name = " " * max_path
  wshort_name = mb_to_wide(short_name)
  size = Win32API.new("kernel32", "GetLongPathNameW", 'PPL', 'L').call(wshort_name, long_name, max_path)
  wide_to_mb(long_name[/.+?(?=\0\0)/]) # up to \0\0
end

Это может решить проблему, указанную в заголовке вопроса и в большей части вашего сообщения, но, возможно, вы задаете неправильный вопрос, и вам следует найти более простое решение для создания временной папки без возврата короткого пути для начала. Таким образом, вам не нужно будет проходить через все это только для того, чтобы решить вашу проблему.

person Toribio    schedule 18.10.2018
comment
Спасибо за ответ, Флавио! Я не подумал о возможности явного вызова широкосимвольной версии путем добавления W к имени функции. Однако я не уверен, как это решит первоначальную проблему. Мне нужно передать Ruby-строку в кодировке UTF-8 в функцию Win32, которая ожидает многобайтность. В приведенном выше примере у меня больше не было бы этой проблемы с фактическим вызовом GetLongPathName(), но я бы просто перенес проблему на свой вызов в MultiByteToWideChar(). Однако, возможно, ребята из windows-pr решили мою проблему, и секрет скрыт где-то в использовании ими Win32API. - person Timm Dapper; 22.10.2018
comment
В качестве дополнения: после более тщательного изучения MultiByteToWideChar() я понимаю, что ошибался. Эта функция принимает в качестве входных данных идентификатор кодовой страницы, поэтому она может (кроме других функций Win32) работать с любой кодировкой, включая UTF-8. Так что то, что вы предлагаете, действительно должно работать. Все еще кажется, что должно быть более элегантное решение, но большое спасибо за указатели! - person Timm Dapper; 22.10.2018

Вместо того, чтобы возиться с Win32 для этого, я бы рекомендовал использовать Sketchup.temp_dir (http://ruby.sketchup.com/Sketchup.html#temp_dir-class_method), чтобы получить системный временный путь и сгенерировать собственное уникальное имя для собственного временного подкаталога. Это будет намного менее хрупко, чем преобразование между локалями и вызовами Win32.

person thomthom    schedule 19.10.2018
comment
К сожалению, это не решает нашу проблему. Sketchup.temp_dir, по-видимому, использует ту же функцию, что и Dir.mktmpdir в Ruby. На всех машинах, на которых я пробовал, обе функции просто возвращают короткое имя папки. - person Timm Dapper; 22.10.2018
comment
Интересно. Я этого не наблюдал. Можете ли вы зарегистрировать проблему в нашем трекере, пожалуйста? github.com/SketchUp/api-issue-tracker/issues - person thomthom; 22.10.2018

Не уверен, что это лучший способ, но вот то, что, похоже, сработало для меня. По сути, я просто изменил функцию из вопроса, чтобы преобразовать ввод в текущую локаль системы и преобразовать результат обратно:

def self.get_long_win32_filename(short_name)
    max_path = 1024
    long_name = " " * max_path
    # Make sure the short_name is in the current system locale encoding,
    # because the Win32 API appears to always expect strings like that.
    short_name = short_name.encode(Encoding::find('locale'))
    lfn_size = Win32API.new("kernel32", "GetLongPathName", ['P','P','L'],'L').call(short_name, long_name, max_path)
    # If lfn_size is a valid value, shorten the long_name to the actual length,
    # otherwise use short_name (e.g. when a zero lfn_size indicates an error).
    long_name = (1..max_path).include?(lfn_size) ? long_name[0..lfn_size-1] : short_name
    # Make sure the return string is in the correct encoding again.
    long_name.force_encoding(Encoding::find('locale'))
    return long_name.encode(Encoding::UTF_8)  
end 

Одна вещь, в которой я не уверен, заключается в том, следует ли мне использовать Encoding::find('locale'), как я делаю, или Encoding::find('filesystem'), потому что мои строки являются именами файлов.

person Timm Dapper    schedule 22.10.2018