Delphi, Subject сертификата и TStrings
Время от времени мне приходится немного программировать на 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, технологиям и так далее и тому подобное, и не дает окончательно их забыть... Вот теперь, действительно, все.
Ссылки на online документацию, которые я даю, относятся к более современным версиям
Delphi
, на сайте Embarcadero я не нашел online документацию поDelphi 6
; в связи с этим следует помнить, что некоторых свойств и методов вDelphi 6
у упоминаемых классов просто нет или они работают чуть по другому. ↩︎