Add project
Basic formatting applied. Unnecessary comments have been removed. Suspicious code is covered by TODO.
This commit is contained in:
@ -0,0 +1,394 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Hcs.ClientApi.FileStoreServiceApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Описание протокола в файле "ГИС ЖКХ. Альбом ТФФ 14.5.0.1.docx"
|
||||
/// 2.5 Описание протокола обмена файлами с внешними системами
|
||||
public class HcsFileStoreServiceApi
|
||||
{
|
||||
public HcsClientConfig Config { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Максимальный размер в байтах части файла, которую разрешено
|
||||
/// загружать на сервер по спецификации протокола
|
||||
/// </summary>
|
||||
private const int MAX_PART_LENGTH = 5242880;
|
||||
|
||||
public HcsFileStoreServiceApi(HcsClientConfig config)
|
||||
{
|
||||
this.Config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Путь к сервису хранения файлов ГИС ЖКХ на серевере API
|
||||
/// </summary>
|
||||
private const string ExtBusFileStoreServiceRest = "ext-bus-file-store-service/rest";
|
||||
|
||||
/// <summary>
|
||||
/// Получение файла ранее загруженного в ГИС ЖКХ
|
||||
/// </summary>
|
||||
public async Task<HcsFile> DownloadFile(
|
||||
Guid fileGuid, HcsFileStoreContext context, CancellationToken token)
|
||||
{
|
||||
long length = await GetFileLength(context, fileGuid, token);
|
||||
if (length <= MAX_PART_LENGTH) return await DownloadSmallFile(fileGuid, context, token);
|
||||
return await DownloadLargeFile(fileGuid, length, context, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение файла по частям (не более 5Мб) по GUID файла
|
||||
/// </summary>
|
||||
private async Task<HcsFile> DownloadLargeFile(
|
||||
Guid fileGuid, long fileSize, HcsFileStoreContext context, CancellationToken token)
|
||||
{
|
||||
if (fileSize <= MAX_PART_LENGTH)
|
||||
throw new ArgumentException("Too short file for partial download");
|
||||
|
||||
string requestUri = ComposeFileDownloadUri(fileGuid, context);
|
||||
|
||||
var resultStream = new MemoryStream();
|
||||
string resultContentType = null;
|
||||
using (var client = BuildHttpClient())
|
||||
{
|
||||
|
||||
long doneSize = 0;
|
||||
while (doneSize < fileSize)
|
||||
{
|
||||
|
||||
long remainderSize = fileSize - doneSize;
|
||||
long partSize = Math.Min(remainderSize, MAX_PART_LENGTH);
|
||||
|
||||
long fromPosition = doneSize;
|
||||
long toPosition = fromPosition + partSize - 1;
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
request.Headers.Range = new RangeHeaderValue(fromPosition, toPosition);
|
||||
|
||||
var response = await SendRequestAsync(client, request, token);
|
||||
resultContentType = response.Content.Headers.ContentType.ToString();
|
||||
|
||||
long? responseSize = response.Content.Headers.ContentLength;
|
||||
if (responseSize == null || (long)responseSize != partSize)
|
||||
throw new HcsException($"Получена часть файла длиной {responseSize}, а запрашивалась длина {partSize}");
|
||||
|
||||
using (var partStream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
partStream.Position = 0;
|
||||
await partStream.CopyToAsync(resultStream);
|
||||
}
|
||||
|
||||
doneSize += partSize;
|
||||
}
|
||||
|
||||
resultStream.Position = 0;
|
||||
return new HcsFile(null, resultContentType, resultStream);
|
||||
}
|
||||
}
|
||||
|
||||
private string ComposeFileDownloadUri(Guid fileGuid, HcsFileStoreContext context)
|
||||
{
|
||||
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(fileGuid)}?getfile";
|
||||
return Config.ComposeEndpointUri(endpointName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение файла одной частью (не более 5Мб) по GUID файла
|
||||
/// </summary>
|
||||
private async Task<HcsFile> DownloadSmallFile(
|
||||
Guid fileGuid, HcsFileStoreContext context, CancellationToken token)
|
||||
{
|
||||
string requestUri = ComposeFileDownloadUri(fileGuid, context);
|
||||
using (var client = BuildHttpClient())
|
||||
{
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
var response = await SendRequestAsync(client, request, token);
|
||||
|
||||
return new HcsFile(
|
||||
null,
|
||||
response.Content.Headers.ContentType.ToString(),
|
||||
await response.Content.ReadAsStreamAsync());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Guid> UploadFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
|
||||
{
|
||||
if (file is null) throw new ArgumentNullException(nameof(file));
|
||||
|
||||
if (file.Length <= MAX_PART_LENGTH)
|
||||
{
|
||||
return await UploadSmallFile(file, context, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await UploadLargeFile(file, context, token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправка большого файла по частям
|
||||
/// </summary>
|
||||
private async Task<Guid> UploadLargeFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
|
||||
{
|
||||
using var client = BuildHttpClient();
|
||||
|
||||
if (file.Length == 0) throw new ArgumentException("Нельзя передавать файл нулевой длины");
|
||||
int numParts = (int)Math.Ceiling((double)file.Length / MAX_PART_LENGTH);
|
||||
|
||||
Config.Log($"Запрашиваю UploadID для большого файла {file.FileName}");
|
||||
Guid uploadId = await QueryNewUploadId(client, context, file, numParts, token);
|
||||
Config.Log($"Получил UploadID {uploadId} для отправки файла {file.FileName} размером {file.Length} байт");
|
||||
|
||||
long partOffset = 0;
|
||||
for (int partNumber = 1; partNumber <= numParts; partNumber++)
|
||||
{
|
||||
|
||||
long lengthLeft = file.Length - partOffset;
|
||||
int partLength = (int)Math.Min(lengthLeft, MAX_PART_LENGTH);
|
||||
var partStream = new HcsPartialStream(file.Stream, partOffset, partLength);
|
||||
|
||||
Config.Log($"Отправляю часть {partNumber}/{numParts} размером {partLength} байт для файла {file.FileName}");
|
||||
await UploadFilePart(client, context, uploadId, partNumber, partStream, token);
|
||||
partOffset += partLength;
|
||||
}
|
||||
|
||||
Config.Log($"Отправляем признак завершения передачи файла {file.FileName}");
|
||||
await CompleteUpload(client, context, uploadId, token);
|
||||
|
||||
Config.Log($"Файл {file.FileName} успешно передан, получен код файла {uploadId}");
|
||||
return uploadId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение кода для загрузки большого файла из нескольких частей менее 5Мб
|
||||
/// </summary>
|
||||
private async Task<Guid> QueryNewUploadId(
|
||||
HttpClient client, HcsFileStoreContext context, HcsFile file, int numParts, CancellationToken token)
|
||||
{
|
||||
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/?upload";
|
||||
string requestUri = Config.ComposeEndpointUri(endpointName);
|
||||
|
||||
var content = new StringContent("");
|
||||
content.Headers.Add("X-Upload-Filename", CleanUploadFileName(file.FileName));
|
||||
content.Headers.Add("X-Upload-Length", file.Length.ToString());
|
||||
content.Headers.Add("X-Upload-Part-Count", numParts.ToString());
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
|
||||
request.Content = content;
|
||||
var response = await SendRequestAsync(client, request, token);
|
||||
return ParseUploadIdFromReponse(response);
|
||||
}
|
||||
|
||||
private async Task UploadFilePart(
|
||||
HttpClient client, HcsFileStoreContext context, Guid uploadId, int partNumber, Stream partStream,
|
||||
CancellationToken token)
|
||||
{
|
||||
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(uploadId)}";
|
||||
string requestUri = Config.ComposeEndpointUri(endpointName);
|
||||
|
||||
var content = new StreamContent(partStream);
|
||||
content.Headers.Add("X-Upload-Partnumber", partNumber.ToString());
|
||||
content.Headers.ContentMD5 = ComputeMD5(partStream);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
|
||||
request.Content = content;
|
||||
await SendRequestAsync(client, request, token);
|
||||
}
|
||||
|
||||
private async Task CompleteUpload(HttpClient client, HcsFileStoreContext context, Guid uploadId, CancellationToken token)
|
||||
{
|
||||
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(uploadId)}?completed";
|
||||
string requestUri = Config.ComposeEndpointUri(endpointName);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
|
||||
await SendRequestAsync(client, request, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загрузка в ГИС ЖКХ файла до 5Мб размером одной операцией
|
||||
/// </summary>
|
||||
private async Task<Guid> UploadSmallFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
|
||||
{
|
||||
using var client = BuildHttpClient();
|
||||
|
||||
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}";
|
||||
string requestUri = Config.ComposeEndpointUri(endpointName);
|
||||
|
||||
Config.Log($"Начинаю upload малого файла [{file.FileName}] типа [{file.ContentType}] длиной {file.Length}");
|
||||
|
||||
if (file.Stream.Length != file.Length)
|
||||
throw new HcsException($"Длина файла {file.Length} не соответствует размеру данных {file.Stream.Length}");
|
||||
|
||||
file.Stream.Position = 0;
|
||||
|
||||
var content = new StreamContent(file.Stream);
|
||||
content.Headers.Add("X-Upload-Filename", CleanUploadFileName(file.FileName));
|
||||
content.Headers.ContentMD5 = ComputeMD5(file.Stream);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
|
||||
request.Content = content;
|
||||
var response = await SendRequestAsync(client, request, token);
|
||||
return ParseUploadIdFromReponse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение информации о загружаемом или загруженном файле
|
||||
/// </summary>
|
||||
public async Task<long> GetFileLength(HcsFileStoreContext context, Guid fileId, CancellationToken token)
|
||||
{
|
||||
using var client = BuildHttpClient();
|
||||
|
||||
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(fileId)}";
|
||||
string requestUri = Config.ComposeEndpointUri(endpointName);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Head, requestUri);
|
||||
var response = await SendRequestAsync(client, request, token);
|
||||
|
||||
long length = 0;
|
||||
var lengthString = SearchResponseHeader(response, "X-Upload-Length");
|
||||
if (!string.IsNullOrEmpty(lengthString) && long.TryParse(lengthString, out length)) return length;
|
||||
throw new HcsException("В ответе сервера не указана длина файла");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает вычисленное значение AttachmentHASH для данных файла @stream
|
||||
/// </summary>
|
||||
public string ComputeAttachmentHash(Stream stream)
|
||||
{
|
||||
var client = Config as HcsClient;
|
||||
if (client == null) throw new HcsException("Не доступен объект HcsClient для вычиления AttachmentHASH");
|
||||
|
||||
// TODO: Проверить комментарий
|
||||
// В декабре 2024 у меня сломалось вычисление AttachmentHASH для файлов, я стал вычислять
|
||||
// явно верным алгоритмом ГОСТ94 для больших файлов уже неверное значение суммы. В январе
|
||||
// 2025 путем перебора вариантов я обнаружил что ГИСЖКХ теперь вычисляет AttachmentHASH
|
||||
// только по первой части большого файла.
|
||||
//int hashSourceMaxLength = MAX_PART_LENGTH;
|
||||
//if (stream.Length <= hashSourceMaxLength) return client.ComputeGost94Hash(stream);
|
||||
//return client.ComputeGost94Hash(new HcsPartialStream(stream, 0, hashSourceMaxLength));
|
||||
|
||||
// 29.01.2025 СТП ГИС ЖКХ ответила что "проведены работы" и теперь
|
||||
// я вижу что они снова вычисляют AttachmantHASH по полному файлу
|
||||
return client.ComputeGost94Hash(stream);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendRequestAsync(
|
||||
HttpClient client, HttpRequestMessage request, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
Config.Log($"Отправляю запрос {request.Method} \"{request.RequestUri}\"...");
|
||||
|
||||
var response = await client.SendAsync(request, token);
|
||||
if (response.IsSuccessStatusCode) return response;
|
||||
throw new HcsException(DescribeResponseError(response));
|
||||
}
|
||||
|
||||
private Guid ParseUploadIdFromReponse(HttpResponseMessage response)
|
||||
{
|
||||
string uploadIdheaderName = "X-Upload-UploadID";
|
||||
var uploadId = SearchResponseHeader(response, uploadIdheaderName);
|
||||
if (uploadId != null) return HcsUtil.ParseGuid(uploadId);
|
||||
throw new HcsException($"В ответе сервера нет заголовка {uploadIdheaderName}");
|
||||
}
|
||||
|
||||
private HttpClient BuildHttpClient()
|
||||
{
|
||||
var _clientHandler = new HttpClientHandler();
|
||||
_clientHandler.ClientCertificates.Add(Config.Certificate);
|
||||
_clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
||||
|
||||
var client = new HttpClient(_clientHandler);
|
||||
client.DefaultRequestHeaders.Accept.Clear();
|
||||
|
||||
client.DefaultRequestHeaders.Add("X-Upload-OrgPPAGUID", Config.OrgPPAGUID);
|
||||
return client;
|
||||
}
|
||||
|
||||
private string DescribeResponseError(HttpResponseMessage response)
|
||||
{
|
||||
string errorHeader = "X-Upload-Error";
|
||||
var message = SearchResponseHeader(response, errorHeader);
|
||||
if (message != null)
|
||||
{
|
||||
if (knownErrors.ContainsKey(message)) return $"{errorHeader}: {message} ({knownErrors[message]})";
|
||||
return $"{errorHeader}: {message}";
|
||||
}
|
||||
|
||||
return $"HTTP response status {response.StatusCode}";
|
||||
}
|
||||
|
||||
private string SearchResponseHeader(HttpResponseMessage response, string headerName)
|
||||
{
|
||||
if (response.Headers.Any(x => x.Key == headerName))
|
||||
{
|
||||
var pair = response.Headers.First(x => x.Key == headerName);
|
||||
if (pair.Value != null && pair.Value.Any())
|
||||
{
|
||||
return pair.Value.First();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> knownErrors = new Dictionary<string, string>() {
|
||||
{ "FieldValidationException", "не пройдены проверки на корректность заполнения полей (обязательность, формат и т.п.)" },
|
||||
{ "FileNotFoundException", "не пройдены проверки на существование файла" },
|
||||
{ "InvalidStatusException", "не пройдены проверки на корректный статус файла" },
|
||||
{ "InvalidSizeException", "некорректный запрашиваемый размер файла" },
|
||||
{ "FileVirusInfectionException", "содержимое файла инфицировано" },
|
||||
{ "FileVirusNotCheckedException", "проверка на вредоносное содержимое не выполнялась" },
|
||||
{ "FilePermissionException", "организация и внешняя система не имеют полномочий на скачивание файла" },
|
||||
{ "DataProviderValidationException", "поставщик данных не найден, заблокирован или неактивен" },
|
||||
{ "CertificateValidationException", "информационная система не найдена по отпечатку или заблокирована" },
|
||||
{ "HashConflictException", "не пройдены проверки на соответствие контрольной сумме" },
|
||||
{ "InvalidPartNumberException", "не пройдены проверки на номер части (номер превышает количество частей, указанных в инициализации)" },
|
||||
{ "ContextNotFoundException", "неверное имя хранилища файлов" },
|
||||
{ "ExtensionException", "недопустимое расширение файла" },
|
||||
{ "DetectionException", "не удалось определить MIME-тип загружаемого файла" },
|
||||
{ "INT002029", "сервис недоступен: выполняются регламентные работы" }
|
||||
};
|
||||
|
||||
private byte[] ComputeMD5(System.IO.Stream stream)
|
||||
{
|
||||
var position = stream.Position;
|
||||
var md5 = System.Security.Cryptography.MD5.Create().ComputeHash(stream);
|
||||
stream.Position = position;
|
||||
return md5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Готовит имя размещаемого файла для помещения в заголовок HTTP-запроса
|
||||
/// </summary>
|
||||
private string CleanUploadFileName(string fileName)
|
||||
{
|
||||
if (fileName is null) return null;
|
||||
|
||||
string bannedSymbols = "<>?:|*%\\\"";
|
||||
|
||||
var buf = new StringBuilder();
|
||||
foreach (char ch in fileName)
|
||||
{
|
||||
if (bannedSymbols.Contains(ch)) buf.Append('_');
|
||||
else buf.Append(ch);
|
||||
}
|
||||
|
||||
// Спецификация предписывает кодировать имя файла по стандарту MIME RFC2047
|
||||
// https://datatracker.ietf.org/doc/html/rfc2047
|
||||
// Как имя кодировки можно явно использовать константу "windows-1251".
|
||||
string characterSet = Encoding.Default.WebName;
|
||||
return EncodedWord.RFC2047.Encode(
|
||||
buf.ToString(), EncodedWord.RFC2047.ContentEncoding.Base64, characterSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
374
Hcs.Client/ClientApi/FileStoreServiceApi/RFC2047.cs
Normal file
374
Hcs.Client/ClientApi/FileStoreServiceApi/RFC2047.cs
Normal file
@ -0,0 +1,374 @@
|
||||
//===============================================================================
|
||||
// RFC2047 (Encoded Word) Decoder
|
||||
// https://github.com/grumpydev/RFC2047-Encoded-Word-Encoder-Decoder/blob/master/EncodedWord/RFC2047.cs
|
||||
// http://tools.ietf.org/html/rfc2047
|
||||
//===============================================================================
|
||||
// Copyright © Steven Robbins. All rights reserved.
|
||||
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
|
||||
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
|
||||
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//===============================================================================
|
||||
|
||||
namespace EncodedWord
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides support for decoding RFC2047 (Encoded Word) encoded text
|
||||
/// </summary>
|
||||
public static class RFC2047
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex for parsing encoded word sections
|
||||
/// From http://tools.ietf.org/html/rfc2047#section-3
|
||||
/// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
||||
/// </summary>
|
||||
private static readonly Regex EncodedWordFormatRegEx = new Regex(@"=\?(?<charset>.*?)\?(?<encoding>[qQbB])\?(?<encodedtext>.*?)\?=", RegexOptions.Singleline | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Regex for removing CRLF SPACE separators from between encoded words
|
||||
/// </summary>
|
||||
private static readonly Regex EncodedWordSeparatorRegEx = new Regex(@"\?=\r\n =\?", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Replacement string for removing CRLF SPACE separators
|
||||
/// </summary>
|
||||
private const string SeparatorReplacement = @"?==?";
|
||||
|
||||
/// <summary>
|
||||
/// The maximum line length allowed
|
||||
/// </summary>
|
||||
private const int MaxLineLength = 75;
|
||||
|
||||
/// <summary>
|
||||
/// Regex for "Q-Encoding" hex bytes from http://tools.ietf.org/html/rfc2047#section-4.2
|
||||
/// </summary>
|
||||
private static readonly Regex QEncodingHexCodeRegEx = new Regex(@"(=(?<hexcode>[0-9a-fA-F][0-9a-fA-F]))", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Regex for replacing _ with space as declared in http://tools.ietf.org/html/rfc2047#section-4.2
|
||||
/// </summary>
|
||||
private static readonly Regex QEncodingSpaceRegEx = new Regex("_", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Format for an encoded string
|
||||
/// </summary>
|
||||
private const string EncodedStringFormat = @"=?{0}?{1}?{2}?=";
|
||||
|
||||
/// <summary>
|
||||
/// Special characters, as defined by RFC2047
|
||||
/// </summary>
|
||||
private static readonly char[] SpecialCharacters = { '(', ')', '<', '>', '@', ',', ';', ':', '<', '>', '/', '[', ']', '?', '.', '=', '\t' };
|
||||
|
||||
/// <summary>
|
||||
/// Represents a content encoding type defined in RFC2047
|
||||
/// </summary>
|
||||
public enum ContentEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown / invalid encoding
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// "Q Encoding" (reduced character set) encoding
|
||||
/// http://tools.ietf.org/html/rfc2047#section-4.2
|
||||
/// </summary>
|
||||
QEncoding,
|
||||
|
||||
/// <summary>
|
||||
/// Base 64 encoding
|
||||
/// http://tools.ietf.org/html/rfc2047#section-4.1
|
||||
/// </summary>
|
||||
Base64
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode a string into RFC2047
|
||||
/// </summary>
|
||||
/// <param name="plainString">Plain string to encode</param>
|
||||
/// <param name="contentEncoding">Content encoding to use</param>
|
||||
/// <param name="characterSet">Character set used by plainString</param>
|
||||
/// <returns>Encoded string</returns>
|
||||
public static string Encode(string plainString, ContentEncoding contentEncoding = ContentEncoding.QEncoding, string characterSet = "iso-8859-1")
|
||||
{
|
||||
if (String.IsNullOrEmpty(plainString))
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
if (contentEncoding == ContentEncoding.Unknown)
|
||||
{
|
||||
throw new ArgumentException("contentEncoding cannot be unknown for encoding.", "contentEncoding");
|
||||
}
|
||||
|
||||
if (!IsSupportedCharacterSet(characterSet))
|
||||
{
|
||||
throw new ArgumentException("characterSet is not supported", "characterSet");
|
||||
}
|
||||
|
||||
var textEncoding = Encoding.GetEncoding(characterSet);
|
||||
|
||||
var encoder = GetContentEncoder(contentEncoding);
|
||||
|
||||
var encodedContent = encoder.Invoke(plainString, textEncoding);
|
||||
|
||||
return BuildEncodedString(characterSet, contentEncoding, encodedContent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode a string containing RFC2047 encoded sections
|
||||
/// </summary>
|
||||
/// <param name="encodedString">String contaning encoded sections</param>
|
||||
/// <returns>Decoded string</returns>
|
||||
public static string Decode(string encodedString)
|
||||
{
|
||||
// Remove separators
|
||||
var decodedString = EncodedWordSeparatorRegEx.Replace(encodedString, SeparatorReplacement);
|
||||
|
||||
return EncodedWordFormatRegEx.Replace(
|
||||
decodedString,
|
||||
m =>
|
||||
{
|
||||
var contentEncoding = GetContentEncodingType(m.Groups["encoding"].Value);
|
||||
if (contentEncoding == ContentEncoding.Unknown)
|
||||
{
|
||||
// Regex should never match, but return anyway
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var characterSet = m.Groups["charset"].Value;
|
||||
if (!IsSupportedCharacterSet(characterSet))
|
||||
{
|
||||
// Fall back to iso-8859-1 if invalid/unsupported character set found
|
||||
characterSet = @"iso-8859-1";
|
||||
}
|
||||
|
||||
var textEncoding = Encoding.GetEncoding(characterSet);
|
||||
var contentDecoder = GetContentDecoder(contentEncoding);
|
||||
var encodedText = m.Groups["encodedtext"].Value;
|
||||
|
||||
return contentDecoder.Invoke(encodedText, textEncoding);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a character set is supported
|
||||
/// </summary>
|
||||
/// <param name="characterSet">Character set name</param>
|
||||
/// <returns>Bool representing whether the character set is supported</returns>
|
||||
private static bool IsSupportedCharacterSet(string characterSet)
|
||||
{
|
||||
return Encoding.GetEncodings()
|
||||
.Where(e => String.Equals(e.Name, characterSet, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content encoding type from the encoding character
|
||||
/// </summary>
|
||||
/// <param name="contentEncodingCharacter">Content contentEncodingCharacter character</param>
|
||||
/// <returns>ContentEncoding type</returns>
|
||||
private static ContentEncoding GetContentEncodingType(string contentEncodingCharacter)
|
||||
{
|
||||
switch (contentEncodingCharacter)
|
||||
{
|
||||
case "Q":
|
||||
case "q":
|
||||
return ContentEncoding.QEncoding;
|
||||
case "B":
|
||||
case "b":
|
||||
return ContentEncoding.Base64;
|
||||
default:
|
||||
return ContentEncoding.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content decoder delegate for the given content encoding type
|
||||
/// </summary>
|
||||
/// <param name="contentEncoding">Content encoding type</param>
|
||||
/// <returns>Decoding delegate</returns>
|
||||
private static Func<string, Encoding, string> GetContentDecoder(ContentEncoding contentEncoding)
|
||||
{
|
||||
switch (contentEncoding)
|
||||
{
|
||||
case ContentEncoding.Base64:
|
||||
return DecodeBase64;
|
||||
case ContentEncoding.QEncoding:
|
||||
return DecodeQEncoding;
|
||||
default:
|
||||
// Will never get here, but return a "null" delegate anyway
|
||||
return (s, e) => String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content encoder delegate for the given content encoding type
|
||||
/// </summary>
|
||||
/// <param name="contentEncoding">Content encoding type</param>
|
||||
/// <returns>Encoding delegate</returns>
|
||||
private static Func<string, Encoding, string> GetContentEncoder(ContentEncoding contentEncoding)
|
||||
{
|
||||
switch (contentEncoding)
|
||||
{
|
||||
case ContentEncoding.Base64:
|
||||
return EncodeBase64;
|
||||
case ContentEncoding.QEncoding:
|
||||
return EncodeQEncoding;
|
||||
default:
|
||||
// Will never get here, but return a "null" delegate anyway
|
||||
return (s, e) => String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a base64 encoded string
|
||||
/// </summary>
|
||||
/// <param name="encodedText">Encoded text</param>
|
||||
/// <param name="textEncoder">Encoding instance for the code page required</param>
|
||||
/// <returns>Decoded string</returns>
|
||||
private static string DecodeBase64(string encodedText, Encoding textEncoder)
|
||||
{
|
||||
var encodedBytes = Convert.FromBase64String(encodedText);
|
||||
|
||||
return textEncoder.GetString(encodedBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a base64 encoded string
|
||||
/// </summary>
|
||||
/// <param name="plainText">Plain text</param>
|
||||
/// <param name="textEncoder">Encoding instance for the code page required</param>
|
||||
/// <returns>Encoded string</returns>
|
||||
private static string EncodeBase64(string plainText, Encoding textEncoder)
|
||||
{
|
||||
var plainTextBytes = textEncoder.GetBytes(plainText);
|
||||
|
||||
return Convert.ToBase64String(plainTextBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a "Q encoded" string
|
||||
/// </summary>
|
||||
/// <param name="encodedText">Encoded text</param>
|
||||
/// <param name="textEncoder">Encoding instance for the code page required</param>
|
||||
/// <returns>Decoded string</returns>
|
||||
private static string DecodeQEncoding(string encodedText, Encoding textEncoder)
|
||||
{
|
||||
var decodedText = QEncodingSpaceRegEx.Replace(encodedText, " ");
|
||||
|
||||
decodedText = QEncodingHexCodeRegEx.Replace(
|
||||
decodedText,
|
||||
m =>
|
||||
{
|
||||
var hexString = m.Groups["hexcode"].Value;
|
||||
|
||||
int characterValue;
|
||||
if (!int.TryParse(hexString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue))
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
return textEncoder.GetString(new[] { (byte)characterValue });
|
||||
});
|
||||
|
||||
return decodedText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a "Q encoded" string
|
||||
/// </summary>
|
||||
/// <param name="plainText">Plain text</param>
|
||||
/// <param name="textEncoder">Encoding instance for the code page required</param>
|
||||
/// <returns>Encoded string</returns>
|
||||
private static string EncodeQEncoding(string plainText, Encoding textEncoder)
|
||||
{
|
||||
if (textEncoder.GetByteCount(plainText) != plainText.Length)
|
||||
{
|
||||
throw new ArgumentException("Q encoding only supports single byte encodings", "textEncoder");
|
||||
}
|
||||
|
||||
var specialBytes = textEncoder.GetBytes(SpecialCharacters);
|
||||
|
||||
var sb = new StringBuilder(plainText.Length);
|
||||
|
||||
var plainBytes = textEncoder.GetBytes(plainText);
|
||||
|
||||
// Replace "high" values
|
||||
for (int i = 0; i < plainBytes.Length; i++)
|
||||
{
|
||||
if (plainBytes[i] <= 127 && !specialBytes.Contains(plainBytes[i]))
|
||||
{
|
||||
sb.Append(Convert.ToChar(plainBytes[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("=");
|
||||
sb.Append(Convert.ToString(plainBytes[i], 16).ToUpper());
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString().Replace(" ", "_");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the full encoded string representation
|
||||
/// </summary>
|
||||
/// <param name="characterSet">Characterset to use</param>
|
||||
/// <param name="contentEncoding">Content encoding to use</param>
|
||||
/// <param name="encodedContent">Content, encoded to the above parameters</param>
|
||||
/// <returns>Valid RFC2047 string</returns>
|
||||
private static string BuildEncodedString(string characterSet, ContentEncoding contentEncoding, string encodedContent)
|
||||
{
|
||||
var encodingCharacter = String.Empty;
|
||||
|
||||
switch (contentEncoding)
|
||||
{
|
||||
case ContentEncoding.Base64:
|
||||
encodingCharacter = "B";
|
||||
break;
|
||||
case ContentEncoding.QEncoding:
|
||||
encodingCharacter = "Q";
|
||||
break;
|
||||
}
|
||||
|
||||
var wrapperLength = string.Format(EncodedStringFormat, characterSet, encodingCharacter, String.Empty).Length;
|
||||
var chunkLength = MaxLineLength - wrapperLength;
|
||||
|
||||
if (encodedContent.Length <= chunkLength)
|
||||
{
|
||||
return string.Format(EncodedStringFormat, characterSet, encodingCharacter, encodedContent);
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var chunk in SplitStringByLength(encodedContent, chunkLength))
|
||||
{
|
||||
sb.AppendFormat(EncodedStringFormat, characterSet, encodingCharacter, chunk);
|
||||
sb.Append("\r\n ");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string into chunks
|
||||
/// </summary>
|
||||
/// <param name="inputString">Input string</param>
|
||||
/// <param name="chunkSize">Size of each chunk</param>
|
||||
/// <returns>String collection of chunked strings</returns>
|
||||
public static IEnumerable<string> SplitStringByLength(this string inputString, int chunkSize)
|
||||
{
|
||||
for (int index = 0; index < inputString.Length; index += chunkSize)
|
||||
{
|
||||
yield return inputString.Substring(index, Math.Min(chunkSize, inputString.Length - index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user