Время от времени мне приходится немного программировать на Delphi. При этом следует отметить, что долгое время Delphi был моим основным профессиональным инструментом - языком и IDE. В свое время он обошел в списке моих предпочтений таких "тяжеловесов" как C и C++. Произошло это в середине 90-ых прошлого тысячелетия (😉) - именно тогда я стал пользоваться первой версией этого замечательного продукта, еще на Windows 3.11. Но затем, году так в 2004, меня заинтересовал C# с .Net, а где-то в 2006 я поучавствовал в первом проекте на Java, и аббревиатура JDK, а с ней и сама платформа, постепенно стали моими новыми фаворитами. И теперь я лишь время от времени правлю что-нибудь в legacy проектах на Delphi.

И вот совсем недавно мне пришлось воспользоваться Delphi 6, чтобы реализовать изменения, которые потребовались пользователям небольшой утилитки. Утилита эта отображает информацию о сертификатах, записанных на специализированных носителях, в качестве которых выступают так называемые токены: у нас это обычно eToken или ruToken. Речь шла о небольшой правке - раньше в окне программки информация о Subject сертификата (все эти Е, L, OU и CN) выводилась в виде строки, а теперь пользователи захотели видеть каждый элемент отдельно, да еще и в определенном порядке. Нужно это им было для визуальной проверки сертификатов, выпущенных нашим авторизованным УЦ.

С точки зрения изменений пользовательского интерфейса вопросов у меня практически не возникло: было понятно, что TMemo надо будет заменить на TStringGrid, TListBox или TListView. Да и над тем, как реализовать нужное преобразование, я размышлял не очень долго. Чтобы понять, почему, достаточно взглянуть на то, как выглядит значение Subject в сертификате. Вот самый обычный пример:

CN="Фамилия Имя; АО ""Название организации""", OU=Подразделение, O="АО ""Название организации""", L=Город, S=Регион, C=Страна, E=some@email.com, GN=Имя Отчество

То есть, это значение из себя представляет набор подстрок в формате key=value, разделенных, если внимательно присмотреться, строками ", " (запятая и пробел). К тому же, часть строк, являющихся значениями (частью value), представлены в так называемом "quoted" формате, при использовании которого в начале и в конце строки ставится cимвол " (двойные кавычки), а если эти самые двойные кавычки встречаются в самой строке, то они удваиваются.

Начнем с того, что со строками в фомате key=value в Delphi можно замечательно работать при помощи класса TStrings[1]: у него есть свойство Values, которое, как раз, и позволяет получить значение value по ключу name. Все очень просто:

...
val := StringsList.Values['key'];
...

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

...
StringsList.CommaText := CertificateSubjectValueString;
...

Эйфория прошла при первом же тестовом прогоне - что все-таки делает время, в течени которого ты не пользуешься инструментом! TStrings в результате заполнился строками, но немного не так, как я ожидал. На самом деле, в документации свойства CommaText все разжевано, но память меня подвела, а заглянуть по приведенной ссылке мне помешала самоуверенность. Проблема была в том, что CommaText под разделителем понимает не только символ , но и пробел и вообще любой непечатный символ.

Первым порывом после неудачи было попробовать свойство DelimitedText, но я вовремя вспомнил, что CommaText и DelimitedText по сути - близнецы-братья, о чем, собственно упоминает и документация, а свойство StrictDelimiter в Delphi 6 еще не изобрели. После осознания этого факта стало понятно, что придется подогнать к нужному виду саму входную строку. И тут, в общем-то, все было довольно очевидно:

  • надо заменить все разделительные строки ", " на простые запятые (,); тут на помощь пришел тот факт, что в наших сертификатах внутри строк-значений (частей "value") последовательность ", " не встречалась
  • надо заменить все оставшиеся пробелы (" ") на какой-нибудь другой символ, главное, чтобы тот не встречался в первоначальной строке, так как придется выполнять обратное преобразование; я выбрал "~" - его в наших сертификата тоже нет
  • надо избавиться от "quoted" строк, чтобы CommaText не применил каких-нибудь своих правил их обработки; я заменил все " на |, так как этот символ в исходных строках не встречался совсем

Приведенные выше преобразования я выполнил последовательным вызовом метода AnsiReplaceStr, причем именно в том порядке, что привел выше. Получившуюся после преобразований строку просто "скормил" CommaText, который замечательно справился со своей задачей - разбил ее на подстроки.

Но это было только частью работы. При заполнении элемента пользовательского интерфейса надо выполнить обратные преобразования - вернуть двойные кавычки и пробелы, после того, как нужное значение добыто из свойства Values. Это обратное преобразование я выполнял при помощи все той же AnsiReplaceStr. А для того, чтобы потом разобраться с "quoted"-строками я воспользовался методом AnsiDequotedStr. Надо отметить, что этот метод выгодно отличается от AnsiExtractQuotedStr тем, что в случае, если исходная строка не является "quoted"-строкой, последний возвращает пустую строку, а использованный мной метод просто возвращает первоначальную строку без изменений. Это поведение подходит мне как нельзя лучше, так как не все значения атрибутов Subject закодированы в виде "quoted"-строк, есть и самые обычные строки. И если бы не было бы AnsiDequotedStr, то пришлось бы городить дополнительные проверки.

А что касается обеспечения необходимого порядка, то я просто производил обращение к свойству Values используя ту последовательность ключей, которую указали пользователи - сертификаты, выпущенные нашим УЦ, обладают еще такой особенностью, что Subject сертификата всегда содержит строго определенный набор полей. Вот, собственно, что получилось:

// Процедура по заполнению экземпляра TStringGrid-а
// значениями полей Subject-а сертификата
// Параметр aSubject - строка Subject-а сертификата
procedure TTSATCMainForm.SplitSubject(aSubject: string);
var
  lst: TStringList;
begin
  lst := TStringList.Create;
  try
    // Заменим символы и присвоим получившееся значение свойству CommaText
    lst.CommaText := AnsiReplaceStr( // Заменим '"' на "|"
                       AnsiReplaceStr( // Заменим ' ' на '~'
                         AnsiReplaceStr(aSubject, ', ', ',') // Заменим ', ' на ','
                         , ' ', '~')
                       ,'"', '|');
    // Установим значение из UnstructuredName
    strgSubject.Cells[0, 0] := 
        GetSubjectElementValue(lst.Values['OID.1.2.840.113549.1.9.2']);
    // Установим значение из CommonName
    strgSubject.Cells[0, 1] := GetSubjectElementValue(lst.Values['CN']);
    // Установим значение из SurName
    strgSubject.Cells[0, 2] := GetSubjectElementValue(lst.Values['SN']);
    // Установим значение из GivenName
    strgSubject.Cells[0, 3] := GetSubjectElementValue(lst.Values['G']);
    // Установим значение из OrganizationalUnit
    strgSubject.Cells[0, 4] := GetSubjectElementValue(lst.Values['OU']);
    // Установим значение из Organization
    strgSubject.Cells[0, 5] := GetSubjectElementValue(lst.Values['O']);
    // Установим значение из State
    strgSubject.Cells[0, 6] := GetSubjectElementValue(lst.Values['S']);
    // Установим значение из Locality
    strgSubject.Cells[0, 7] := GetSubjectElementValue(lst.Values['L']);
    // Установим значение из Country
    strgSubject.Cells[0, 8] := GetSubjectElementValue(lst.Values['C']);
    // Установим значение из EMail
    strgSubject.Cells[0, 9] := GetSubjectElementValue(lst.Values['E']);
    // Установим значение из ИНН
    strgSubject.Cells[0, 10] := GetSubjectElementValue(lst.Values['ИНН']);
    // Установим значение из ОГРН
    strgSubject.Cells[0, 11] := GetSubjectElementValue(lst.Values['ОГРН']);
    // Установим значение из СНИЛС
    strgSubject.Cells[0, 12] := GetSubjectElementValue(lst.Values['СНИЛС']);
  finally
    FreeAndNil(lst);
  end;
end;

// Функция получения значения для ячейки экземпляра TStringGrid-а
// Параметр anElement - преобразованная ранее строка значения
// Возвращаемое значение - строка для ячейки
function TTSATCMainForm.GetSubjectElementValue(anElement: string): string;
begin
    result := AnsiDequotedStr( // преобразуем "quoted" строку
                AnsiReplaceStr( // заменим '|' на '"'
                  AnsiReplaceStr(anElement, '~', ' '), // заменим '~' на ' '
                  '|', '"'),
                '"');
end;

Вот, пожалуй, и все. Зачем я, собственно, рассказал об этом случае. Самое главное в этой истории то, что если долго не пользуешься инструментом, даже тем, который, казалось бы, знаешь вдоль и поперек, детали начинают стираться. А потому, на мой взгляд, в поддержке legacy проектов есть польза - она позволяет время от времени возвращаться к используемым ранее языкам, IDE, технологиям и так далее и тому подобное, и не дает окончательно их забыть... Вот теперь, действительно, все.


  1. Ссылки на online документацию, которые я даю, относятся к более современным версиям Delphi, на сайте Embarcadero я не нашел online документацию по Delphi 6; в связи с этим следует помнить, что некоторых свойств и методов в Delphi 6 у упоминаемых классов просто нет или они работают чуть по другому. ↩︎