Как определить, содержит ли строка недопустимые закодированные символы - PullRequest
31 голосов
/ 20 мая 2009

Сценарий использования

Мы внедрили веб-сервис, который разработчики нашего веб-интерфейса используют (через php api) для отображения данных о продукте. На веб-сайте пользователь вводит что-то (то есть строку запроса). Внутренне сайт совершает звонок в сервис через API.

Примечание: мы используем restlet, а не tomcat

Исходная задача

Firefox 3.0.10, похоже, учитывает выбранную кодировку в браузере и кодирует URL в соответствии с выбранной кодировкой. Это приводит к различным строкам запроса для ISO-8859-1 и UTF-8.

Наш веб-сайт перенаправляет ввод от пользователя и не преобразует его (что должно), поэтому он может сделать вызов службе через API, вызывающий веб-сервис, используя строку запроса, содержащую умлауты на немецком языке.

т.е. для части запроса, выглядящей как

    ...v=abcädef

если выбран «ISO-8859-1», отправленная часть запроса выглядит как

...v=abc%E4def

но если выбрано "UTF-8", отправленная часть запроса выглядит как

...v=abc%C3%A4def

Желаемое решение

Поскольку мы контролируем службу, потому что мы реализовали ее, мы хотим проверить на стороне сервера , не содержит ли вызов не utf-8 символов, если так, ответьте http 4xx статусом

Текущее решение в деталях

Проверка для каждого символа (== string.substring (i, i + 1))

  1. если character.getBytes () [0] равно 63 для '?'
  2. если Character.getType (character.charAt (0)) возвращает OTHER_SYMBOL

Код

protected List< String > getNonUnicodeCharacters( String s ) {
  final List< String > result = new ArrayList< String >();
  for ( int i = 0 , n = s.length() ; i < n ; i++ ) {
    final String character = s.substring( i , i + 1 );
    final boolean isOtherSymbol = 
      ( int ) Character.OTHER_SYMBOL
       == Character.getType( character.charAt( 0 ) );
    final boolean isNonUnicode = isOtherSymbol 
      && character.getBytes()[ 0 ] == ( byte ) 63;
    if ( isNonUnicode )
      result.add( character );
  }
  return result;
}

Вопрос

Будет ли это перехватывать все недопустимые (не в кодировке utf) символы? У кого-нибудь из вас есть лучшее (более простое) решение?

Примечание: Я проверил URLDecoder со следующим кодом

final String[] test = new String[]{
  "v=abc%E4def",
  "v=abc%C3%A4def"
};
for ( int i = 0 , n = test.length ; i < n ; i++ ) {
    System.out.println( java.net.URLDecoder.decode(test[i],"UTF-8") );
    System.out.println( java.net.URLDecoder.decode(test[i],"ISO-8859-1") );
}

Это печатает:

v=abc?def
v=abcädef
v=abcädef
v=abcädef

и не создает исключение IllegalArgumentException вздох

Ответы [ 10 ]

31 голосов
/ 19 сентября 2009

Я задавал тот же вопрос,

Обработка кодировки символов в URI на Tomcat

Я недавно нашел решение, и оно работает довольно хорошо для меня. Возможно, вы захотите попробовать. Вот что вам нужно сделать,

  1. Оставьте кодировку URI как Latin-1. В Tomcat добавьте URIEncoding = "ISO-8859-1" к соединителю в файле server.xml.
  2. Если вам нужно вручную декодировать URL, используйте Latin1 в качестве набора символов.
  3. Используйте функцию fixEncoding () для исправления кодировок.

Например, чтобы получить параметр из строки запроса,

  String name = fixEncoding(request.getParameter("name"));

Вы можете сделать это всегда. Строка с правильной кодировкой не изменяется.

Код прилагается. Удачи!

 public static String fixEncoding(String latin1) {
  try {
   byte[] bytes = latin1.getBytes("ISO-8859-1");
   if (!validUTF8(bytes))
    return latin1;   
   return new String(bytes, "UTF-8");  
  } catch (UnsupportedEncodingException e) {
   // Impossible, throw unchecked
   throw new IllegalStateException("No Latin1 or UTF-8: " + e.getMessage());
  }

 }

 public static boolean validUTF8(byte[] input) {
  int i = 0;
  // Check for BOM
  if (input.length >= 3 && (input[0] & 0xFF) == 0xEF
    && (input[1] & 0xFF) == 0xBB & (input[2] & 0xFF) == 0xBF) {
   i = 3;
  }

  int end;
  for (int j = input.length; i < j; ++i) {
   int octet = input[i];
   if ((octet & 0x80) == 0) {
    continue; // ASCII
   }

   // Check for UTF-8 leading byte
   if ((octet & 0xE0) == 0xC0) {
    end = i + 1;
   } else if ((octet & 0xF0) == 0xE0) {
    end = i + 2;
   } else if ((octet & 0xF8) == 0xF0) {
    end = i + 3;
   } else {
    // Java only supports BMP so 3 is max
    return false;
   }

   while (i < end) {
    i++;
    octet = input[i];
    if ((octet & 0xC0) != 0x80) {
     // Not a valid trailing byte
     return false;
    }
   }
  }
  return true;
 }

РЕДАКТИРОВАТЬ: Ваш подход не работает по разным причинам. Когда есть ошибки кодирования, вы не можете рассчитывать на то, что вы получаете от Tomcat. Иногда вы получаете или? В других случаях вы ничего не получите, getParameter () возвращает ноль. Скажем, вы можете проверить «?», Что произойдет, ваша строка запроса содержит действительный «?»

Кроме того, вы не должны отклонять любой запрос. Это не вина вашего пользователя. Как я уже упоминал в своем первоначальном вопросе, браузер может кодировать URL-адрес в формате UTF-8 или Latin-1. Пользователь не имеет контроля. Вы должны принять оба. Изменение вашего сервлета на Latin-1 сохранит всех персонажей, даже если они не правы, чтобы дать нам возможность исправить это или выбросить.

Решение, которое я выложил здесь, не является идеальным, но это лучшее, что мы нашли.

14 голосов
/ 24 сентября 2009

Вы можете использовать CharsetDecoder, настроенный на выдачу исключения, если найдены недопустимые символы:

 CharsetDecoder UTF8Decoder =
      Charset.forName("UTF8").newDecoder().onMalformedInput(CodingErrorAction.REPORT);

См. CodingErrorAction.REPORT

5 голосов
/ 22 января 2015

Это то, что я использовал для проверки кодировки:

CharsetDecoder ebcdicDecoder = Charset.forName("IBM1047").newDecoder();
ebcdicDecoder.onMalformedInput(CodingErrorAction.REPORT);
ebcdicDecoder.onUnmappableCharacter(CodingErrorAction.REPORT);

CharBuffer out = CharBuffer.wrap(new char[3200]);
CoderResult result = ebcdicDecoder.decode(ByteBuffer.wrap(bytes), out, true);
if (result.isError() || result.isOverflow() ||
    result.isUnderflow() || result.isMalformed() ||
    result.isUnmappable())
{
    System.out.println("Cannot decode EBCDIC");
}
else
{
    CoderResult result = ebcdicDecoder.flush(out);
    if (result.isOverflow())
       System.out.println("Cannot decode EBCDIC");
    if (result.isUnderflow())
        System.out.println("Ebcdic decoded succefully ");
}

Редактировать: дополнено предложением Vouze

4 голосов
/ 05 июля 2012

Заменить все контрольные символы в пустую строку

value = value.replaceAll("\\p{Cntrl}", "");
3 голосов
/ 23 сентября 2009

Я работал над похожей проблемой «угадать кодировку». Лучшее решение включает , зная кодировку. За исключением этого, вы можете сделать обоснованные предположения, чтобы различать UTF-8 и ISO-8859-1.

Чтобы ответить на общий вопрос о том, как определить, правильно ли закодирована строка UTF-8, вы можете проверить следующее:

  1. Нет байтов 0x00, 0xC0, 0xC1 или в диапазоне 0xF5-0xFF.
  2. Хвостовым байтам (0x80-0xBF) всегда предшествует главный байт 0xC2-0xF4 или другой хвостовой байт.
  3. Головные байты должны правильно предсказывать количество хвостовых байтов (например, любой байт в 0xC2-0xDF должен сопровождаться ровно одним байтом в диапазоне 0x80-0xBF).

Если строка проходит все эти тесты, то она интерпретируется как допустимая UTF-8. Это не гарантирует, что будет UTF-8, но это хороший прогноз.

Правильный ввод в ISO-8859-1, скорее всего, не будет содержать управляющих символов (0x00-0x1F и 0x80-0x9F), кроме разделителей строк. Похоже, 0x7F также не определен в ISO-8859-1.

(Я основываюсь на страницах Википедии для UTF-8 и ISO-8859-1.)

3 голосов
/ 20 мая 2009

URLDecoder будет декодироваться в заданную кодировку. Это должно помечать ошибки соответствующим образом. Однако в документации говорится:

Есть два возможных способа, которыми этот декодер может работать с недопустимыми строками. Он может оставить недопустимые символы в одиночку или вызвать исключение IllegalArgumentException. Какой подход использует декодер, остается до реализации.

Так что, наверное, тебе стоит попробовать. Обратите внимание также (из документации метода decode ()):

Рекомендация консорциума World Wide Web гласит, что следует использовать UTF-8. Несоблюдение этого требования может привести к несовместимости

так что есть еще о чем подумать!

РЕДАКТИРОВАТЬ: Apache Commons URLDecode утверждает, что выдает соответствующие исключения для неправильных кодировок.

2 голосов
/ 25 сентября 2009

Возможно, вы захотите включить в ваши запросы известный параметр, например, «... & encTest = ä €», чтобы безопасно различать разные кодировки.

1 голос
/ 23 сентября 2009

Вас может заинтересовать следующее регулярное выражение:

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/185624

Я использую его в ruby ​​следующим образом:

module Encoding
    UTF8RGX = /\A(
        [\x09\x0A\x0D\x20-\x7E]            # ASCII
      | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
      |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
      | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
      |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
      |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
      | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
      |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
    )*\z/x unless defined? UTF8RGX

    def self.utf8_file?(fileName)
      count = 0
      File.open("#{fileName}").each do |l|
        count += 1
        unless utf8_string?(l)
          puts count.to_s + ": " + l
        end
      end
      return true
    end

    def self.utf8_string?(a_string)
      UTF8RGX === a_string
    end

end
1 голос
/ 19 сентября 2009

Вам необходимо настроить кодировку с самого начала. Попробуйте отправить правильный заголовок Content-Type , например Content-Type: text / html; charset = utf-8 для исправления правильной кодировки. Стандарт соответствия ссылается на utf-8 и utf-16 как правильную кодировку для веб-служб. Изучите заголовки ваших ответов.

Кроме того, на стороне сервера & mdash; в случае, если браузер неправильно обрабатывает кодировку, отправленную сервером & mdash; форсировать кодирование, выделив новую строку. Также вы можете проверить каждый байт в кодированной строке utf-8, выполнив один each_byte & 0x80 , подтвердив результат как ненулевой.


boolean utfEncoded = true;
byte[] strBytes = queryString.getBytes();
for (int i = 0; i < strBytes.length(); i++) {
    if ((strBytes[i] & 0x80) != 0) {
        continue;
    } else {
        /* treat the string as non utf encoded */
        utfEncoded = false;
        break;
    }
}

String realQueryString = utfEncoded ?
    queryString : new String(queryString.getBytes(), "iso-8859-1");

Кроме того, взгляните на эту статью , надеюсь, она вам поможет.

0 голосов
/ 24 сентября 2009

Попробуйте использовать UTF-8 по умолчанию, как всегда в любом месте, куда вы можете прикоснуться. (База данных, память и пользовательский интерфейс)

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

...