first commit
This commit is contained in:
14
DCIT.Core/DCIT.Core.csproj
Normal file
14
DCIT.Core/DCIT.Core.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RootNamespace>DCIT.Core</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Svg" Version="3.4.7" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
98
DCIT.Core/Models/AppConfig.cs
Normal file
98
DCIT.Core/Models/AppConfig.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public class AppConfig
|
||||
{
|
||||
public string Format { get; set; } = "dcit.config";
|
||||
|
||||
public string Version { get; set; } = "1.0";
|
||||
|
||||
public DateTime SavedAt { get; set; } = DateTime.Now;
|
||||
|
||||
public AppSettings Settings { get; set; } = new AppSettings();
|
||||
|
||||
public ReplacePageState ReplacePage { get; set; } = new ReplacePageState();
|
||||
|
||||
public RestorePageState RestorePage { get; set; } = new RestorePageState();
|
||||
}
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public bool JsonDiffEncryption { get; set; } = true;
|
||||
|
||||
public bool EnableImageReplacement { get; set; } = true;
|
||||
|
||||
public bool UseFixedImageSeed { get; set; }
|
||||
|
||||
public int FixedImageSeed { get; set; } = 20260424;
|
||||
|
||||
public AutoExtractSettings AutoExtract { get; set; } = new AutoExtractSettings();
|
||||
}
|
||||
|
||||
public class AutoExtractSettings
|
||||
{
|
||||
public bool ExtractKeywords { get; set; } = true;
|
||||
|
||||
public bool ExtractHighFrequencyWords { get; set; } = true;
|
||||
|
||||
public bool ExtractHighFrequencySentences { get; set; } = true;
|
||||
|
||||
public bool UseKeywordFrequency { get; set; } = true;
|
||||
|
||||
public bool UseKeywordTfIdf { get; set; } = true;
|
||||
|
||||
public bool UseKeywordTextRank { get; set; } = true;
|
||||
|
||||
public int MaxKeywordCount { get; set; } = 10;
|
||||
|
||||
public int MaxWordCount { get; set; } = 10;
|
||||
|
||||
public int MaxSentenceCount { get; set; } = 10;
|
||||
|
||||
public bool CountChineseWords { get; set; } = true;
|
||||
|
||||
public bool CountEnglishWords { get; set; } = true;
|
||||
|
||||
public bool CountChineseSentences { get; set; } = true;
|
||||
|
||||
public bool CountEnglishSentences { get; set; } = true;
|
||||
|
||||
public int MinChineseWordLength { get; set; } = 1;
|
||||
|
||||
public int MinEnglishWordLength { get; set; } = 1;
|
||||
|
||||
public string SentenceDelimiters { get; set; } = "。!?;.!?;\r\n";
|
||||
|
||||
public bool ExcludeEmailAddresses { get; set; }
|
||||
}
|
||||
|
||||
public class ReplacePageState
|
||||
{
|
||||
public string SourceDirectory { get; set; } = string.Empty;
|
||||
|
||||
public string OutputDirectory { get; set; } = string.Empty;
|
||||
|
||||
public string RuleFilePath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class RestorePageState
|
||||
{
|
||||
public string ReplaceDirectory { get; set; } = string.Empty;
|
||||
|
||||
public string RestoreDirectory { get; set; } = string.Empty;
|
||||
|
||||
public string DiffInputFormat { get; set; } = ".diff";
|
||||
}
|
||||
|
||||
public class ConfigurationLoadResult
|
||||
{
|
||||
public AppConfig Config { get; set; } = new AppConfig();
|
||||
|
||||
public bool IsMissing { get; set; }
|
||||
|
||||
public bool IsCorrupted { get; set; }
|
||||
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
}
|
||||
99
DCIT.Core/Models/AutoExtractModels.cs
Normal file
99
DCIT.Core/Models/AutoExtractModels.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public class ExtractionProgress
|
||||
{
|
||||
public int Current { get; set; }
|
||||
|
||||
public int Total { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public class ExtractedDocumentText
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
|
||||
public string VisibleText { get; set; }
|
||||
}
|
||||
|
||||
public class ExtractionCandidate : INotifyPropertyChanged
|
||||
{
|
||||
private bool _append = true;
|
||||
private AutoExtractCategory _primaryCategory;
|
||||
private string _categoryDisplay;
|
||||
private string _originalText;
|
||||
private string _statisticDisplay;
|
||||
private string _firstDocumentPath;
|
||||
private int _firstOccurrenceOrder;
|
||||
|
||||
public bool Append
|
||||
{
|
||||
get { return _append; }
|
||||
set { SetField(ref _append, value); }
|
||||
}
|
||||
|
||||
public AutoExtractCategory PrimaryCategory
|
||||
{
|
||||
get { return _primaryCategory; }
|
||||
set { SetField(ref _primaryCategory, value); }
|
||||
}
|
||||
|
||||
public string CategoryDisplay
|
||||
{
|
||||
get { return _categoryDisplay; }
|
||||
set { SetField(ref _categoryDisplay, value); }
|
||||
}
|
||||
|
||||
public string OriginalText
|
||||
{
|
||||
get { return _originalText; }
|
||||
set { SetField(ref _originalText, value); }
|
||||
}
|
||||
|
||||
public string StatisticDisplay
|
||||
{
|
||||
get { return _statisticDisplay; }
|
||||
set { SetField(ref _statisticDisplay, value); }
|
||||
}
|
||||
|
||||
public string FirstDocumentPath
|
||||
{
|
||||
get { return _firstDocumentPath; }
|
||||
set { SetField(ref _firstDocumentPath, value); }
|
||||
}
|
||||
|
||||
public int FirstOccurrenceOrder
|
||||
{
|
||||
get { return _firstOccurrenceOrder; }
|
||||
set { SetField(ref _firstOccurrenceOrder, value); }
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
public class AutoExtractResult
|
||||
{
|
||||
public List<ExtractionCandidate> Candidates { get; } = new List<ExtractionCandidate>();
|
||||
|
||||
public List<string> Warnings { get; } = new List<string>();
|
||||
|
||||
public int DocumentsProcessed { get; set; }
|
||||
|
||||
public int DocumentsSkipped { get; set; }
|
||||
}
|
||||
}
|
||||
51
DCIT.Core/Models/BatchExecutionModels.cs
Normal file
51
DCIT.Core/Models/BatchExecutionModels.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public enum BatchExecutionSkipReason
|
||||
{
|
||||
DuplicateSourcePath = 0,
|
||||
DuplicateOutputPath = 1,
|
||||
DuplicateDiffPath = 2
|
||||
}
|
||||
|
||||
public sealed class BatchExecutionSkipItem
|
||||
{
|
||||
public FileScanItem Item { get; set; }
|
||||
|
||||
public BatchExecutionSkipReason Reason { get; set; }
|
||||
|
||||
public string ConflictingPath { get; set; }
|
||||
|
||||
public string ExistingPath { get; set; }
|
||||
|
||||
public string Message
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Reason)
|
||||
{
|
||||
case BatchExecutionSkipReason.DuplicateSourcePath:
|
||||
return "源文件与批次中已有任务重复,已跳过。";
|
||||
case BatchExecutionSkipReason.DuplicateOutputPath:
|
||||
return "输出目标与批次中已有任务冲突,已跳过。";
|
||||
case BatchExecutionSkipReason.DuplicateDiffPath:
|
||||
return "差异文件目标与批次中已有任务冲突,已跳过。";
|
||||
default:
|
||||
return "批处理任务冲突,已跳过。";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BatchExecutionPlan
|
||||
{
|
||||
public IList<FileScanItem> ExecutionItems { get; set; } = new List<FileScanItem>();
|
||||
|
||||
public IList<BatchExecutionSkipItem> SkippedItems { get; set; } = new List<BatchExecutionSkipItem>();
|
||||
|
||||
public int DegreeOfParallelism { get; set; } = 1;
|
||||
|
||||
public bool RequiresSerialExecution { get; set; }
|
||||
}
|
||||
}
|
||||
222
DCIT.Core/Models/DiffModels.cs
Normal file
222
DCIT.Core/Models/DiffModels.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public class DiffDocument
|
||||
{
|
||||
public string Format { get; set; } = "dcit-diff";
|
||||
|
||||
public string Version { get; set; } = "1.0";
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.Now;
|
||||
|
||||
public bool Encrypted { get; set; }
|
||||
|
||||
public DiffAppInfo App { get; set; } = new DiffAppInfo();
|
||||
|
||||
public DiffAlgorithmInfo Algorithm { get; set; } = new DiffAlgorithmInfo();
|
||||
|
||||
public DiffDocumentInfo SourceDocument { get; set; } = new DiffDocumentInfo();
|
||||
|
||||
public DiffDocumentInfo ReplacedDocument { get; set; } = new DiffDocumentInfo();
|
||||
|
||||
public DiffRuleFileInfo RuleFile { get; set; } = new DiffRuleFileInfo();
|
||||
|
||||
public DiffIntegrityInfo Integrity { get; set; } = new DiffIntegrityInfo();
|
||||
|
||||
public DiffEncryptionInfo Encryption { get; set; }
|
||||
|
||||
public string PayloadCiphertext { get; set; }
|
||||
|
||||
public DiffPayload Payload { get; set; }
|
||||
}
|
||||
|
||||
public class DiffAppInfo
|
||||
{
|
||||
public string Name { get; set; } = "DCIT";
|
||||
|
||||
public string Version { get; set; } = "1.0.0";
|
||||
}
|
||||
|
||||
public class DiffAlgorithmInfo
|
||||
{
|
||||
public string DiffVersion { get; set; } = "1.0";
|
||||
|
||||
public string BmpCodecVersion { get; set; } = "1.0";
|
||||
}
|
||||
|
||||
public class DiffDocumentInfo
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
|
||||
public string RelativePath { get; set; }
|
||||
|
||||
public string Extension { get; set; }
|
||||
|
||||
public string FingerprintAlgorithm { get; set; } = "SHA-256";
|
||||
|
||||
public string Fingerprint { get; set; }
|
||||
}
|
||||
|
||||
public class DiffRuleFileInfo
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
|
||||
public string Version { get; set; } = "1.0";
|
||||
|
||||
public string FingerprintAlgorithm { get; set; } = "SHA-256";
|
||||
|
||||
public string Fingerprint { get; set; }
|
||||
}
|
||||
|
||||
public class DiffIntegrityInfo
|
||||
{
|
||||
public string HashAlgorithm { get; set; } = "SHA-256";
|
||||
|
||||
public string PayloadHash { get; set; }
|
||||
|
||||
public string TamperProtectionAlgorithm { get; set; } = "HMAC-SHA256";
|
||||
|
||||
public string TamperProtectionValue { get; set; }
|
||||
}
|
||||
|
||||
public class DiffEncryptionInfo
|
||||
{
|
||||
public string Algorithm { get; set; } = "AES-256-CBC+HMAC-SHA256";
|
||||
|
||||
public string KeyId { get; set; } = "default";
|
||||
|
||||
public string Nonce { get; set; }
|
||||
|
||||
public string Tag { get; set; }
|
||||
}
|
||||
|
||||
public class DiffPayload
|
||||
{
|
||||
public DiffStatistics Statistics { get; set; } = new DiffStatistics();
|
||||
|
||||
public List<DiffOperation> Operations { get; set; } = new List<DiffOperation>();
|
||||
}
|
||||
|
||||
public class DiffStatistics
|
||||
{
|
||||
public int TextReplaceCount { get; set; }
|
||||
|
||||
public int ImageReplaceCount { get; set; }
|
||||
}
|
||||
|
||||
public class DiffOperation
|
||||
{
|
||||
public string OperationId { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public string ObjectType { get; set; }
|
||||
|
||||
public string ContainerKind { get; set; }
|
||||
|
||||
public string EmbeddedProgramId { get; set; }
|
||||
|
||||
public string EmbeddedPackageExtension { get; set; }
|
||||
|
||||
public DiffPosition Position { get; set; } = new DiffPosition();
|
||||
|
||||
public DiffRuleHitInfo Rule { get; set; }
|
||||
|
||||
public string OriginalText { get; set; }
|
||||
|
||||
public string ReplacementTemplateResult { get; set; }
|
||||
|
||||
public string WrittenText { get; set; }
|
||||
|
||||
public int? DisplayWidthOriginal { get; set; }
|
||||
|
||||
public int? DisplayWidthWritten { get; set; }
|
||||
|
||||
public bool? Truncated { get; set; }
|
||||
|
||||
public bool? PaddingApplied { get; set; }
|
||||
|
||||
public string SourceKind { get; set; }
|
||||
|
||||
public DiffImageData OriginalImage { get; set; }
|
||||
|
||||
public DiffImageData ConvertedBitmapImage { get; set; }
|
||||
|
||||
public DiffImageData NewImage { get; set; }
|
||||
|
||||
public int? Seed { get; set; }
|
||||
|
||||
public DiffNoiseSummary NoiseSummary { get; set; }
|
||||
|
||||
public DiffImageLayout Layout { get; set; }
|
||||
}
|
||||
|
||||
public class DiffPosition
|
||||
{
|
||||
public string StoryType { get; set; }
|
||||
|
||||
public string ContainerPath { get; set; }
|
||||
|
||||
public int ParagraphIndex { get; set; }
|
||||
|
||||
public int ParagraphTextLength { get; set; } = -1;
|
||||
|
||||
public string ParagraphTextHash { get; set; }
|
||||
|
||||
public int StartTextOffset { get; set; } = -1;
|
||||
|
||||
public int EndTextOffset { get; set; } = -1;
|
||||
|
||||
public int StartRunIndex { get; set; }
|
||||
|
||||
public int StartCharOffset { get; set; }
|
||||
|
||||
public int EndRunIndex { get; set; }
|
||||
|
||||
public int EndCharOffset { get; set; }
|
||||
|
||||
public int? ObjectIndex { get; set; }
|
||||
}
|
||||
|
||||
public class DiffRuleHitInfo
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string MatchMode { get; set; }
|
||||
|
||||
public string Pattern { get; set; }
|
||||
|
||||
public int HitIndex { get; set; }
|
||||
}
|
||||
|
||||
public class DiffImageData
|
||||
{
|
||||
public string Format { get; set; }
|
||||
|
||||
public int WidthPx { get; set; }
|
||||
|
||||
public int HeightPx { get; set; }
|
||||
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public class DiffNoiseSummary
|
||||
{
|
||||
public double ModifiedPixelRatio { get; set; }
|
||||
}
|
||||
|
||||
public class DiffImageLayout
|
||||
{
|
||||
public long DisplayWidthEmu { get; set; }
|
||||
|
||||
public long DisplayHeightEmu { get; set; }
|
||||
|
||||
public string WrapMode { get; set; }
|
||||
|
||||
public double RotationDegree { get; set; }
|
||||
}
|
||||
}
|
||||
53
DCIT.Core/Models/Enums.cs
Normal file
53
DCIT.Core/Models/Enums.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public enum RuleMatchMode
|
||||
{
|
||||
PlainText = 0,
|
||||
RegularExpression = 1
|
||||
}
|
||||
|
||||
public enum ProcessingStatus
|
||||
{
|
||||
Pending = 0,
|
||||
InProgress = 1,
|
||||
Success = 2,
|
||||
Failed = 3,
|
||||
Stopped = 4
|
||||
}
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Info = 0,
|
||||
Warning = 1,
|
||||
Error = 2
|
||||
}
|
||||
|
||||
public enum AutoExtractCategory
|
||||
{
|
||||
Keyword = 0,
|
||||
HighFrequencyWord = 1,
|
||||
HighFrequencySentence = 2
|
||||
}
|
||||
|
||||
public enum KeywordAlgorithm
|
||||
{
|
||||
Frequency = 0,
|
||||
TfIdf = 1,
|
||||
TextRank = 2
|
||||
}
|
||||
|
||||
public enum RuleValidationSeverity
|
||||
{
|
||||
Info = 0,
|
||||
Warning = 1,
|
||||
Error = 2
|
||||
}
|
||||
|
||||
public enum RuleTarget
|
||||
{
|
||||
DocumentContent = 0,
|
||||
FileName = 1
|
||||
}
|
||||
}
|
||||
72
DCIT.Core/Models/FileScanItem.cs
Normal file
72
DCIT.Core/Models/FileScanItem.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public class FileScanItem : INotifyPropertyChanged
|
||||
{
|
||||
private bool _isSelected = true;
|
||||
private string _sourcePath;
|
||||
private string _outputPath;
|
||||
private string _diffPath;
|
||||
private int _textOperationCount;
|
||||
private int _imageOperationCount;
|
||||
private ProcessingStatus _status = ProcessingStatus.Pending;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get { return _isSelected; }
|
||||
set { SetField(ref _isSelected, value); }
|
||||
}
|
||||
|
||||
public string SourcePath
|
||||
{
|
||||
get { return _sourcePath; }
|
||||
set { SetField(ref _sourcePath, value); }
|
||||
}
|
||||
|
||||
public string OutputPath
|
||||
{
|
||||
get { return _outputPath; }
|
||||
set { SetField(ref _outputPath, value); }
|
||||
}
|
||||
|
||||
public string DiffPath
|
||||
{
|
||||
get { return _diffPath; }
|
||||
set { SetField(ref _diffPath, value); }
|
||||
}
|
||||
|
||||
public int TextOperationCount
|
||||
{
|
||||
get { return _textOperationCount; }
|
||||
set { SetField(ref _textOperationCount, value); }
|
||||
}
|
||||
|
||||
public int ImageOperationCount
|
||||
{
|
||||
get { return _imageOperationCount; }
|
||||
set { SetField(ref _imageOperationCount, value); }
|
||||
}
|
||||
|
||||
public ProcessingStatus Status
|
||||
{
|
||||
get { return _status; }
|
||||
set { SetField(ref _status, value); }
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
17
DCIT.Core/Models/LogEntry.cs
Normal file
17
DCIT.Core/Models/LogEntry.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public class LogEntry
|
||||
{
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
public string EventCode { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string Detail { get; set; }
|
||||
}
|
||||
}
|
||||
20
DCIT.Core/Models/LogWriteFailureEventArgs.cs
Normal file
20
DCIT.Core/Models/LogWriteFailureEventArgs.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public sealed class LogWriteFailureEventArgs : EventArgs
|
||||
{
|
||||
public LogWriteFailureEventArgs(LogEntry entry, string logFilePath, Exception exception)
|
||||
{
|
||||
Entry = entry;
|
||||
LogFilePath = logFilePath;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public LogEntry Entry { get; private set; }
|
||||
|
||||
public string LogFilePath { get; private set; }
|
||||
|
||||
public Exception Exception { get; private set; }
|
||||
}
|
||||
}
|
||||
72
DCIT.Core/Models/ProcessingModels.cs
Normal file
72
DCIT.Core/Models/ProcessingModels.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public class ReplaceFileRequest
|
||||
{
|
||||
public string SourcePath { get; set; }
|
||||
|
||||
public string OutputPath { get; set; }
|
||||
|
||||
public string DiffPath { get; set; }
|
||||
|
||||
public string SourceRootDirectory { get; set; }
|
||||
|
||||
public string RuleFilePath { get; set; }
|
||||
|
||||
public RuleFile RuleFile { get; set; }
|
||||
|
||||
public bool EncryptDiff { get; set; }
|
||||
|
||||
public bool EnableImageReplacement { get; set; } = true;
|
||||
|
||||
public int? FixedImageSeed { get; set; }
|
||||
}
|
||||
|
||||
public class RestoreFileRequest
|
||||
{
|
||||
public string ReplacedPath { get; set; }
|
||||
|
||||
public string DiffPath { get; set; }
|
||||
|
||||
public string OutputPath { get; set; }
|
||||
|
||||
public string ReplaceRootDirectory { get; set; }
|
||||
|
||||
public string CurrentRuleFilePath { get; set; }
|
||||
}
|
||||
|
||||
public class FileProcessProgress
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
|
||||
public int FilePercent { get; set; }
|
||||
|
||||
public string CurrentRuleId { get; set; }
|
||||
|
||||
public string CurrentRuleName { get; set; }
|
||||
|
||||
public int CurrentFileTextCount { get; set; }
|
||||
|
||||
public int TotalTextCount { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public bool IsWarning { get; set; }
|
||||
}
|
||||
|
||||
public class FileProcessResult
|
||||
{
|
||||
public string OutputPath { get; set; }
|
||||
|
||||
public string DiffPath { get; set; }
|
||||
|
||||
public string BmpPath { get; set; }
|
||||
|
||||
public int TextOperationCount { get; set; }
|
||||
|
||||
public int ImageOperationCount { get; set; }
|
||||
|
||||
public bool Stopped { get; set; }
|
||||
}
|
||||
}
|
||||
125
DCIT.Core/Models/RuleDefinition.cs
Normal file
125
DCIT.Core/Models/RuleDefinition.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public class RuleDefinition : INotifyPropertyChanged
|
||||
{
|
||||
private string _id;
|
||||
private string _name;
|
||||
private bool _isEnabled = true;
|
||||
private RuleMatchMode _matchMode = RuleMatchMode.PlainText;
|
||||
private bool _isCaseSensitive;
|
||||
private bool _isWholeWord;
|
||||
private RuleTarget _target = RuleTarget.DocumentContent;
|
||||
private string _searchText;
|
||||
private string _replacementText;
|
||||
private string _note;
|
||||
|
||||
public string Id
|
||||
{
|
||||
get { return _id; }
|
||||
set { SetField(ref _id, value); }
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return _name; }
|
||||
set { SetField(ref _name, value); }
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get { return _isEnabled; }
|
||||
set { SetField(ref _isEnabled, value); }
|
||||
}
|
||||
|
||||
public RuleMatchMode MatchMode
|
||||
{
|
||||
get { return _matchMode; }
|
||||
set { SetField(ref _matchMode, value); }
|
||||
}
|
||||
|
||||
public RuleTarget Target
|
||||
{
|
||||
get { return _target; }
|
||||
set { SetField(ref _target, value); }
|
||||
}
|
||||
|
||||
public bool IsCaseSensitive
|
||||
{
|
||||
get { return _isCaseSensitive; }
|
||||
set { SetField(ref _isCaseSensitive, value); }
|
||||
}
|
||||
|
||||
public bool IsWholeWord
|
||||
{
|
||||
get { return _isWholeWord; }
|
||||
set { SetField(ref _isWholeWord, value); }
|
||||
}
|
||||
|
||||
public string SearchText
|
||||
{
|
||||
get { return _searchText; }
|
||||
set { SetField(ref _searchText, value); }
|
||||
}
|
||||
|
||||
public string ReplacementText
|
||||
{
|
||||
get { return _replacementText; }
|
||||
set { SetField(ref _replacementText, value); }
|
||||
}
|
||||
|
||||
public string Note
|
||||
{
|
||||
get { return _note; }
|
||||
set { SetField(ref _note, value); }
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
public class RuleFile
|
||||
{
|
||||
public string Format { get; set; } = "dcit.rule";
|
||||
|
||||
public string Version { get; set; } = "1.0";
|
||||
|
||||
public List<RuleDefinition> Rules { get; set; } = new List<RuleDefinition>();
|
||||
}
|
||||
|
||||
public class RuleValidationIssue
|
||||
{
|
||||
public RuleValidationSeverity Severity { get; set; }
|
||||
|
||||
public int Index { get; set; }
|
||||
|
||||
public string RuleId { get; set; }
|
||||
|
||||
public string RuleName { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public class RuleValidationReport
|
||||
{
|
||||
public List<RuleValidationIssue> Issues { get; } = new List<RuleValidationIssue>();
|
||||
|
||||
public bool HasErrors
|
||||
{
|
||||
get { return Issues.Exists(item => item.Severity == RuleValidationSeverity.Error); }
|
||||
}
|
||||
}
|
||||
}
|
||||
24
DCIT.Core/Models/RuntimeEnvironmentValidationModels.cs
Normal file
24
DCIT.Core/Models/RuntimeEnvironmentValidationModels.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DCIT.Core.Models
|
||||
{
|
||||
public sealed class RuntimeEnvironmentValidationResult
|
||||
{
|
||||
public string StartupDirectory { get; set; }
|
||||
|
||||
public bool IsStartupDirectoryWritable { get; set; }
|
||||
|
||||
public string ResolvedAutomationProgId { get; set; }
|
||||
|
||||
public string ResolvedDocToPath { get; set; }
|
||||
|
||||
public IList<string> BlockingIssues { get; } = new List<string>();
|
||||
|
||||
public IList<string> Warnings { get; } = new List<string>();
|
||||
|
||||
public bool HasBlockingIssues
|
||||
{
|
||||
get { return BlockingIssues.Count > 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
550
DCIT.Core/Services/AutoExtractService.cs
Normal file
550
DCIT.Core/Services/AutoExtractService.cs
Normal file
@@ -0,0 +1,550 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DCIT.Core.Models;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class AutoExtractService
|
||||
{
|
||||
private static readonly Regex TokenRegex = new Regex(
|
||||
@"(?<Email>[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})|(?<English>[A-Za-z][A-Za-z0-9_\-']*)|(?<Chinese>[\u4E00-\u9FFF]+)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private readonly WordTextExtractionService _wordTextExtractionService;
|
||||
|
||||
public AutoExtractService(WordTextExtractionService wordTextExtractionService)
|
||||
{
|
||||
_wordTextExtractionService = wordTextExtractionService;
|
||||
}
|
||||
|
||||
public Task<AutoExtractResult> ExtractAsync(
|
||||
IList<string> selectedFiles,
|
||||
AutoExtractSettings settings,
|
||||
IList<RuleDefinition> existingRules,
|
||||
IProgress<ExtractionProgress> progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(
|
||||
() => Execute(selectedFiles, settings, existingRules, progress, cancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private AutoExtractResult Execute(
|
||||
IList<string> selectedFiles,
|
||||
AutoExtractSettings settings,
|
||||
IList<RuleDefinition> existingRules,
|
||||
IProgress<ExtractionProgress> progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new AutoExtractResult();
|
||||
var extractedDocuments = new List<ExtractedDocumentText>();
|
||||
|
||||
if (selectedFiles == null || selectedFiles.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
settings = settings ?? new AutoExtractSettings();
|
||||
existingRules = existingRules ?? Array.Empty<RuleDefinition>();
|
||||
|
||||
for (var index = 0; index < selectedFiles.Count; index++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var filePath = selectedFiles[index];
|
||||
progress?.Report(new ExtractionProgress
|
||||
{
|
||||
Current = index + 1,
|
||||
Total = selectedFiles.Count,
|
||||
Message = "正在分析 " + filePath,
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var extracted = _wordTextExtractionService.ExtractVisibleText(filePath);
|
||||
if (!string.IsNullOrWhiteSpace(extracted.VisibleText))
|
||||
{
|
||||
extractedDocuments.Add(extracted);
|
||||
result.DocumentsProcessed++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.DocumentsSkipped++;
|
||||
result.Warnings.Add(filePath + " 未提取到可见文本。");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.DocumentsSkipped++;
|
||||
result.Warnings.Add(filePath + " 提取失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
if (extractedDocuments.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var candidateMap = new Dictionary<string, ExtractionCandidate>(StringComparer.Ordinal);
|
||||
var existingSearchTexts = new HashSet<string>(
|
||||
existingRules
|
||||
.Where(rule => !string.IsNullOrWhiteSpace(rule.SearchText))
|
||||
.Select(rule => rule.SearchText),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
if (settings.ExtractKeywords)
|
||||
{
|
||||
foreach (var keyword in BuildKeywordCandidates(extractedDocuments, settings))
|
||||
{
|
||||
AddCandidate(candidateMap, existingSearchTexts, keyword);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.ExtractHighFrequencyWords)
|
||||
{
|
||||
foreach (var word in BuildHighFrequencyWordCandidates(extractedDocuments, settings))
|
||||
{
|
||||
AddCandidate(candidateMap, existingSearchTexts, word);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.ExtractHighFrequencySentences)
|
||||
{
|
||||
foreach (var sentence in BuildSentenceCandidates(extractedDocuments, settings))
|
||||
{
|
||||
AddCandidate(candidateMap, existingSearchTexts, sentence);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var candidate in candidateMap.Values.OrderBy(item => item.FirstOccurrenceOrder))
|
||||
{
|
||||
result.Candidates.Add(candidate);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<ExtractionCandidate> BuildKeywordCandidates(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
|
||||
{
|
||||
var tokenObservations = BuildTokenObservations(documents, settings).ToList();
|
||||
if (tokenObservations.Count == 0)
|
||||
{
|
||||
return Enumerable.Empty<ExtractionCandidate>();
|
||||
}
|
||||
|
||||
var candidates = new List<ExtractionCandidate>();
|
||||
if (settings.UseKeywordFrequency)
|
||||
{
|
||||
candidates.AddRange(CreateCandidatesFromRankedEntries(
|
||||
RankByFrequency(tokenObservations).Take(settings.MaxKeywordCount),
|
||||
AutoExtractCategory.Keyword,
|
||||
"关键字(词频)"));
|
||||
}
|
||||
|
||||
if (settings.UseKeywordTfIdf)
|
||||
{
|
||||
candidates.AddRange(CreateCandidatesFromRankedEntries(
|
||||
RankByTfIdf(tokenObservations).Take(settings.MaxKeywordCount),
|
||||
AutoExtractCategory.Keyword,
|
||||
"关键字(TF-IDF)"));
|
||||
}
|
||||
|
||||
if (settings.UseKeywordTextRank)
|
||||
{
|
||||
candidates.AddRange(CreateCandidatesFromRankedEntries(
|
||||
RankByTextRank(tokenObservations).Take(settings.MaxKeywordCount),
|
||||
AutoExtractCategory.Keyword,
|
||||
"关键字(TextRank)"));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private IEnumerable<ExtractionCandidate> BuildHighFrequencyWordCandidates(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
|
||||
{
|
||||
var tokenObservations = BuildTokenObservations(documents, settings).ToList();
|
||||
return CreateCandidatesFromRankedEntries(
|
||||
RankByFrequency(tokenObservations).Take(settings.MaxWordCount),
|
||||
AutoExtractCategory.HighFrequencyWord,
|
||||
"高频词");
|
||||
}
|
||||
|
||||
private IEnumerable<ExtractionCandidate> BuildSentenceCandidates(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
|
||||
{
|
||||
var sentenceMap = new Dictionary<string, RankedEntry>(StringComparer.Ordinal);
|
||||
var order = 0;
|
||||
var delimiters = new HashSet<char>((settings.SentenceDelimiters ?? string.Empty).ToCharArray());
|
||||
|
||||
foreach (var document in documents)
|
||||
{
|
||||
foreach (var sentence in SplitSentences(document.VisibleText, delimiters))
|
||||
{
|
||||
var trimmed = sentence.Trim();
|
||||
if (!IsAllowedSentence(trimmed, settings))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
order++;
|
||||
RankedEntry entry;
|
||||
if (!sentenceMap.TryGetValue(trimmed, out entry))
|
||||
{
|
||||
entry = new RankedEntry
|
||||
{
|
||||
Text = trimmed,
|
||||
Count = 0,
|
||||
Score = 0,
|
||||
FirstDocumentPath = document.FilePath,
|
||||
FirstOccurrenceOrder = order,
|
||||
};
|
||||
sentenceMap[trimmed] = entry;
|
||||
}
|
||||
|
||||
entry.Count++;
|
||||
entry.Score = entry.Count;
|
||||
}
|
||||
}
|
||||
|
||||
return CreateCandidatesFromRankedEntries(
|
||||
sentenceMap.Values
|
||||
.OrderByDescending(item => item.Count)
|
||||
.ThenBy(item => item.FirstOccurrenceOrder)
|
||||
.Take(settings.MaxSentenceCount),
|
||||
AutoExtractCategory.HighFrequencySentence,
|
||||
"高频句");
|
||||
}
|
||||
|
||||
private IEnumerable<TokenObservation> BuildTokenObservations(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
|
||||
{
|
||||
var observations = new List<TokenObservation>();
|
||||
var order = 0;
|
||||
|
||||
for (var documentIndex = 0; documentIndex < documents.Count; documentIndex++)
|
||||
{
|
||||
var document = documents[documentIndex];
|
||||
foreach (Match match in TokenRegex.Matches(document.VisibleText ?? string.Empty))
|
||||
{
|
||||
if (match.Groups["Email"].Success)
|
||||
{
|
||||
if (!settings.ExcludeEmailAddresses)
|
||||
{
|
||||
order++;
|
||||
observations.Add(CreateTokenObservation(match.Value, document.FilePath, documentIndex, order));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match.Groups["English"].Success)
|
||||
{
|
||||
if (settings.CountEnglishWords && match.Value.Length >= settings.MinEnglishWordLength)
|
||||
{
|
||||
order++;
|
||||
observations.Add(CreateTokenObservation(match.Value, document.FilePath, documentIndex, order));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match.Groups["Chinese"].Success && settings.CountChineseWords)
|
||||
{
|
||||
foreach (var token in SegmentChinese(match.Value, settings.MinChineseWordLength))
|
||||
{
|
||||
if (!ContainsChinese(token))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
order++;
|
||||
observations.Add(CreateTokenObservation(token, document.FilePath, documentIndex, order));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return observations;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SegmentChinese(string text, int minLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var normalized = new string(text.Where(character => character >= 0x4E00 && character <= 0x9FFF).ToArray());
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var effectiveMinLength = Math.Max(minLength, 1);
|
||||
var maxLength = Math.Min(4, normalized.Length);
|
||||
var emitted = false;
|
||||
|
||||
for (var length = maxLength; length >= effectiveMinLength; length--)
|
||||
{
|
||||
for (var index = 0; index + length <= normalized.Length; index++)
|
||||
{
|
||||
emitted = true;
|
||||
yield return normalized.Substring(index, length);
|
||||
}
|
||||
}
|
||||
|
||||
if (!emitted && normalized.Length >= effectiveMinLength)
|
||||
{
|
||||
yield return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<RankedEntry> RankByFrequency(IList<TokenObservation> observations)
|
||||
{
|
||||
return observations
|
||||
.GroupBy(item => item.Token, StringComparer.Ordinal)
|
||||
.Select(group => new RankedEntry
|
||||
{
|
||||
Text = group.Key,
|
||||
Count = group.Count(),
|
||||
Score = group.Count(),
|
||||
FirstDocumentPath = group.OrderBy(item => item.GlobalOrder).First().FilePath,
|
||||
FirstOccurrenceOrder = group.Min(item => item.GlobalOrder),
|
||||
})
|
||||
.OrderByDescending(item => item.Score)
|
||||
.ThenBy(item => item.FirstOccurrenceOrder)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<RankedEntry> RankByTfIdf(IList<TokenObservation> observations)
|
||||
{
|
||||
var documentCount = observations.Select(item => item.DocumentIndex).Distinct().Count();
|
||||
var byToken = observations.GroupBy(item => item.Token, StringComparer.Ordinal);
|
||||
var ranked = new List<RankedEntry>();
|
||||
|
||||
foreach (var tokenGroup in byToken)
|
||||
{
|
||||
var docFrequency = tokenGroup.Select(item => item.DocumentIndex).Distinct().Count();
|
||||
var termFrequency = tokenGroup.Count();
|
||||
var idf = Math.Log((documentCount + 1.0) / (docFrequency + 1.0)) + 1.0;
|
||||
ranked.Add(new RankedEntry
|
||||
{
|
||||
Text = tokenGroup.Key,
|
||||
Count = termFrequency,
|
||||
Score = termFrequency * idf,
|
||||
FirstDocumentPath = tokenGroup.OrderBy(item => item.GlobalOrder).First().FilePath,
|
||||
FirstOccurrenceOrder = tokenGroup.Min(item => item.GlobalOrder),
|
||||
});
|
||||
}
|
||||
|
||||
return ranked
|
||||
.OrderByDescending(item => item.Score)
|
||||
.ThenBy(item => item.FirstOccurrenceOrder)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<RankedEntry> RankByTextRank(IList<TokenObservation> observations)
|
||||
{
|
||||
var graph = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
var orderedTokens = observations.OrderBy(item => item.GlobalOrder).ToList();
|
||||
const int windowSize = 4;
|
||||
|
||||
for (var index = 0; index < orderedTokens.Count; index++)
|
||||
{
|
||||
var current = orderedTokens[index].Token;
|
||||
if (!graph.ContainsKey(current))
|
||||
{
|
||||
graph[current] = new HashSet<string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
for (var offset = 1; offset < windowSize && index + offset < orderedTokens.Count; offset++)
|
||||
{
|
||||
var neighbor = orderedTokens[index + offset].Token;
|
||||
if (string.Equals(current, neighbor, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
graph[current].Add(neighbor);
|
||||
if (!graph.ContainsKey(neighbor))
|
||||
{
|
||||
graph[neighbor] = new HashSet<string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
graph[neighbor].Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
var scores = graph.Keys.ToDictionary(key => key, key => 1.0, StringComparer.Ordinal);
|
||||
for (var iteration = 0; iteration < 20; iteration++)
|
||||
{
|
||||
var next = new Dictionary<string, double>(StringComparer.Ordinal);
|
||||
foreach (var node in graph.Keys)
|
||||
{
|
||||
var score = 0.15;
|
||||
foreach (var neighbor in graph[node])
|
||||
{
|
||||
var degree = Math.Max(graph[neighbor].Count, 1);
|
||||
score += 0.85 * (scores[neighbor] / degree);
|
||||
}
|
||||
|
||||
next[node] = score;
|
||||
}
|
||||
|
||||
scores = next;
|
||||
}
|
||||
|
||||
return observations
|
||||
.GroupBy(item => item.Token, StringComparer.Ordinal)
|
||||
.Select(group => new RankedEntry
|
||||
{
|
||||
Text = group.Key,
|
||||
Count = group.Count(),
|
||||
Score = scores.ContainsKey(group.Key) ? scores[group.Key] : 0,
|
||||
FirstDocumentPath = group.OrderBy(item => item.GlobalOrder).First().FilePath,
|
||||
FirstOccurrenceOrder = group.Min(item => item.GlobalOrder),
|
||||
})
|
||||
.OrderByDescending(item => item.Score)
|
||||
.ThenBy(item => item.FirstOccurrenceOrder)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<ExtractionCandidate> CreateCandidatesFromRankedEntries(
|
||||
IEnumerable<RankedEntry> entries,
|
||||
AutoExtractCategory category,
|
||||
string categoryDisplay)
|
||||
{
|
||||
return entries.Select(entry => new ExtractionCandidate
|
||||
{
|
||||
PrimaryCategory = category,
|
||||
CategoryDisplay = categoryDisplay,
|
||||
OriginalText = entry.Text,
|
||||
StatisticDisplay = string.Format("次数={0}; 分值={1:F3}", entry.Count, entry.Score),
|
||||
FirstDocumentPath = entry.FirstDocumentPath,
|
||||
FirstOccurrenceOrder = entry.FirstOccurrenceOrder,
|
||||
});
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitSentences(string text, HashSet<char> delimiters)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var buffer = string.Empty;
|
||||
foreach (var character in text)
|
||||
{
|
||||
if (delimiters.Contains(character))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(buffer))
|
||||
{
|
||||
yield return buffer;
|
||||
buffer = string.Empty;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer += character;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(buffer))
|
||||
{
|
||||
yield return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAllowedSentence(string sentence, AutoExtractSettings settings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sentence))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasChinese = ContainsChinese(sentence);
|
||||
var hasEnglish = sentence.Any(character => (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z'));
|
||||
|
||||
if (hasChinese && settings.CountChineseSentences)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasEnglish && settings.CountEnglishSentences)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !hasChinese && !hasEnglish;
|
||||
}
|
||||
|
||||
private static bool ContainsChinese(string value)
|
||||
{
|
||||
return value.Any(character => character >= 0x4E00 && character <= 0x9FFF);
|
||||
}
|
||||
|
||||
private static TokenObservation CreateTokenObservation(string token, string filePath, int documentIndex, int order)
|
||||
{
|
||||
return new TokenObservation
|
||||
{
|
||||
Token = token,
|
||||
FilePath = filePath,
|
||||
DocumentIndex = documentIndex,
|
||||
GlobalOrder = order,
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddCandidate(
|
||||
IDictionary<string, ExtractionCandidate> candidateMap,
|
||||
ISet<string> existingSearchTexts,
|
||||
ExtractionCandidate candidate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate.OriginalText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingSearchTexts.Contains(candidate.OriginalText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ExtractionCandidate existing;
|
||||
if (!candidateMap.TryGetValue(candidate.OriginalText, out existing))
|
||||
{
|
||||
candidateMap[candidate.OriginalText] = candidate;
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.CategoryDisplay.IndexOf(candidate.CategoryDisplay, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
existing.CategoryDisplay += ", " + candidate.CategoryDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenObservation
|
||||
{
|
||||
public string Token { get; set; }
|
||||
|
||||
public string FilePath { get; set; }
|
||||
|
||||
public int DocumentIndex { get; set; }
|
||||
|
||||
public int GlobalOrder { get; set; }
|
||||
}
|
||||
|
||||
private class RankedEntry
|
||||
{
|
||||
public string Text { get; set; }
|
||||
|
||||
public int Count { get; set; }
|
||||
|
||||
public double Score { get; set; }
|
||||
|
||||
public string FirstDocumentPath { get; set; }
|
||||
|
||||
public int FirstOccurrenceOrder { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
122
DCIT.Core/Services/BatchExecutionPlanner.cs
Normal file
122
DCIT.Core/Services/BatchExecutionPlanner.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DCIT.Core.Models;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public sealed class BatchExecutionPlanner
|
||||
{
|
||||
public BatchExecutionPlan CreatePlan(IList<FileScanItem> selectedItems, int processorCount)
|
||||
{
|
||||
var executionItems = new List<FileScanItem>();
|
||||
var skippedItems = new List<BatchExecutionSkipItem>();
|
||||
var sourcePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var outputPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var diffPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (selectedItems != null)
|
||||
{
|
||||
foreach (var item in selectedItems)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourcePath = NormalizePath(item.SourcePath);
|
||||
var outputPath = NormalizePath(item.OutputPath);
|
||||
var diffPath = NormalizePath(item.DiffPath);
|
||||
|
||||
var collision = GetCollision(sourcePath, sourcePaths, BatchExecutionSkipReason.DuplicateSourcePath);
|
||||
if (collision == null)
|
||||
{
|
||||
collision = GetCollision(outputPath, outputPaths, BatchExecutionSkipReason.DuplicateOutputPath);
|
||||
}
|
||||
|
||||
if (collision == null && !string.IsNullOrWhiteSpace(diffPath))
|
||||
{
|
||||
collision = GetCollision(diffPath, diffPaths, BatchExecutionSkipReason.DuplicateDiffPath);
|
||||
}
|
||||
|
||||
if (collision != null)
|
||||
{
|
||||
collision.Item = item;
|
||||
skippedItems.Add(collision);
|
||||
continue;
|
||||
}
|
||||
|
||||
AddIfPresent(sourcePaths, sourcePath);
|
||||
AddIfPresent(outputPaths, outputPath);
|
||||
AddIfPresent(diffPaths, diffPath);
|
||||
executionItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
var requiresSerialExecution = executionItems.Any(item => Path.GetExtension(item.SourcePath).Equals(".doc", StringComparison.OrdinalIgnoreCase));
|
||||
var degreeOfParallelism = DetermineDegreeOfParallelism(executionItems.Count, processorCount, requiresSerialExecution);
|
||||
|
||||
return new BatchExecutionPlan
|
||||
{
|
||||
ExecutionItems = executionItems,
|
||||
SkippedItems = skippedItems,
|
||||
DegreeOfParallelism = degreeOfParallelism,
|
||||
RequiresSerialExecution = requiresSerialExecution,
|
||||
};
|
||||
}
|
||||
|
||||
private static BatchExecutionSkipItem GetCollision(string path, HashSet<string> existingPaths, BatchExecutionSkipReason reason)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !existingPaths.Contains(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BatchExecutionSkipItem
|
||||
{
|
||||
Reason = reason,
|
||||
ConflictingPath = path,
|
||||
ExistingPath = path,
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddIfPresent(HashSet<string> set, string path)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
set.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static int DetermineDegreeOfParallelism(int itemCount, int processorCount, bool requiresSerialExecution)
|
||||
{
|
||||
if (itemCount <= 0 || requiresSerialExecution)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var normalizedProcessorCount = processorCount > 0 ? processorCount : Environment.ProcessorCount;
|
||||
normalizedProcessorCount = Math.Max(1, normalizedProcessorCount);
|
||||
return Math.Max(1, Math.Min(itemCount, normalizedProcessorCount));
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Path.GetFullPath(path)
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return path.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
240
DCIT.Core/Services/BmpDiffCodec.cs
Normal file
240
DCIT.Core/Services/BmpDiffCodec.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using DCIT.Core.Utilities;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class BmpDiffCodec
|
||||
{
|
||||
private const string Magic = "DCITBMP1";
|
||||
private const ushort HeaderVersion = 1;
|
||||
private const ushort Flags = 0;
|
||||
private const int FixedWidth = 1024;
|
||||
private const int HeaderSize = 8 + 2 + 2 + 8 + 32;
|
||||
|
||||
public void EncodeJson(string canonicalJson, string outputPath)
|
||||
{
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(canonicalJson ?? string.Empty);
|
||||
using (var stream = new MemoryStream(payloadBytes))
|
||||
{
|
||||
EncodeFromStream(stream, outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
public void EncodeFromStream(Stream jsonStream, string outputPath)
|
||||
{
|
||||
if (jsonStream == null)
|
||||
{
|
||||
throw new ArgumentNullException("jsonStream");
|
||||
}
|
||||
|
||||
byte[] hash;
|
||||
long length;
|
||||
|
||||
if (jsonStream.CanSeek)
|
||||
{
|
||||
length = jsonStream.Length;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
hash = sha.ComputeHash(jsonStream);
|
||||
}
|
||||
|
||||
jsonStream.Position = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var buffered = new MemoryStream())
|
||||
{
|
||||
jsonStream.CopyTo(buffered);
|
||||
length = buffered.Length;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
hash = sha.ComputeHash(buffered);
|
||||
}
|
||||
|
||||
buffered.Position = 0;
|
||||
WriteBitmapWithPayload(outputPath, length, hash, buffered);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WriteBitmapWithPayload(outputPath, length, hash, jsonStream);
|
||||
}
|
||||
|
||||
public string DecodeJson(string bmpPath)
|
||||
{
|
||||
using (var bitmap = new Bitmap(bmpPath))
|
||||
{
|
||||
var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
|
||||
var bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, bitmap.PixelFormat);
|
||||
try
|
||||
{
|
||||
var rowLength = bitmap.Width;
|
||||
var pixelBytes = new byte[rowLength * bitmap.Height];
|
||||
for (var row = 0; row < bitmap.Height; row++)
|
||||
{
|
||||
var source = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride);
|
||||
Marshal.Copy(source, pixelBytes, row * rowLength, rowLength);
|
||||
}
|
||||
|
||||
var payloadBytes = ParseBlob(pixelBytes);
|
||||
return Encoding.UTF8.GetString(payloadBytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap.UnlockBits(bitmapData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteBitmapWithPayload(string outputPath, long payloadLength, byte[] payloadHash, Stream payloadStream)
|
||||
{
|
||||
var totalBytes = HeaderSize + payloadLength;
|
||||
var height = Math.Max(1, (int)Math.Ceiling(totalBytes / (double)FixedWidth));
|
||||
var pixelCount = FixedWidth * height;
|
||||
var pixelBytes = new byte[pixelCount];
|
||||
|
||||
var offset = 0;
|
||||
var magicBytes = Encoding.ASCII.GetBytes(Magic);
|
||||
Buffer.BlockCopy(magicBytes, 0, pixelBytes, offset, magicBytes.Length);
|
||||
offset += magicBytes.Length;
|
||||
|
||||
pixelBytes[offset++] = (byte)(HeaderVersion & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((HeaderVersion >> 8) & 0xFF);
|
||||
|
||||
pixelBytes[offset++] = (byte)(Flags & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((Flags >> 8) & 0xFF);
|
||||
|
||||
pixelBytes[offset++] = (byte)(payloadLength & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((payloadLength >> 8) & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((payloadLength >> 16) & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((payloadLength >> 24) & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((payloadLength >> 32) & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((payloadLength >> 40) & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((payloadLength >> 48) & 0xFF);
|
||||
pixelBytes[offset++] = (byte)((payloadLength >> 56) & 0xFF);
|
||||
|
||||
Buffer.BlockCopy(payloadHash, 0, pixelBytes, offset, 32);
|
||||
offset += 32;
|
||||
|
||||
var buffer = new byte[65536];
|
||||
int read;
|
||||
while ((read = payloadStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
if (offset + read > pixelBytes.Length)
|
||||
{
|
||||
throw new InvalidDataException("BMP 载荷长度超出预期像素缓冲。");
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(buffer, 0, pixelBytes, offset, read);
|
||||
offset += read;
|
||||
}
|
||||
|
||||
if (offset != totalBytes)
|
||||
{
|
||||
throw new InvalidDataException("BMP 载荷长度与声明的长度不一致。");
|
||||
}
|
||||
|
||||
using (var bitmap = new Bitmap(FixedWidth, height, PixelFormat.Format8bppIndexed))
|
||||
{
|
||||
var palette = bitmap.Palette;
|
||||
for (var index = 0; index < 256; index++)
|
||||
{
|
||||
palette.Entries[index] = Color.FromArgb(index, index, index);
|
||||
}
|
||||
|
||||
bitmap.Palette = palette;
|
||||
var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
|
||||
var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
|
||||
try
|
||||
{
|
||||
for (var row = 0; row < height; row++)
|
||||
{
|
||||
var sourceOffset = row * FixedWidth;
|
||||
var destination = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride);
|
||||
Marshal.Copy(pixelBytes, sourceOffset, destination, FixedWidth);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap.UnlockBits(bitmapData);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? string.Empty);
|
||||
bitmap.Save(outputPath, ImageFormat.Bmp);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildBlob(byte[] payloadBytes)
|
||||
{
|
||||
payloadBytes = payloadBytes ?? new byte[0];
|
||||
var payloadHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes));
|
||||
using (var stream = new MemoryStream())
|
||||
using (var writer = new BinaryWriter(stream, Encoding.ASCII))
|
||||
{
|
||||
writer.Write(Encoding.ASCII.GetBytes(Magic));
|
||||
writer.Write(HeaderVersion);
|
||||
writer.Write(Flags);
|
||||
writer.Write((ulong)payloadBytes.Length);
|
||||
writer.Write(payloadHash);
|
||||
writer.Write(payloadBytes);
|
||||
writer.Flush();
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ParseBlob(byte[] blobBytes)
|
||||
{
|
||||
using (var stream = new MemoryStream(blobBytes))
|
||||
using (var reader = new BinaryReader(stream, Encoding.ASCII))
|
||||
{
|
||||
var magic = Encoding.ASCII.GetString(reader.ReadBytes(8));
|
||||
if (!string.Equals(magic, Magic, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException("BMP 差异文件魔数无效。");
|
||||
}
|
||||
|
||||
var version = reader.ReadUInt16();
|
||||
if (version != HeaderVersion)
|
||||
{
|
||||
throw new InvalidDataException("BMP 差异文件版本不受支持。");
|
||||
}
|
||||
|
||||
_ = reader.ReadUInt16();
|
||||
var payloadLength = reader.ReadUInt64();
|
||||
var payloadHash = reader.ReadBytes(32);
|
||||
var payloadBytes = reader.ReadBytes((int)payloadLength);
|
||||
var actualHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes));
|
||||
if (!BytesEqual(payloadHash, actualHash))
|
||||
{
|
||||
throw new InvalidDataException("BMP 差异文件载荷校验失败。");
|
||||
}
|
||||
|
||||
return payloadBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool BytesEqual(byte[] left, byte[] right)
|
||||
{
|
||||
if (left == null || right == null || left.Length != right.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < left.Length; index++)
|
||||
{
|
||||
if (left[index] != right[index])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
DCIT.Core/Services/ConfigurationService.cs
Normal file
66
DCIT.Core/Services/ConfigurationService.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using DCIT.Core.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class ConfigurationService
|
||||
{
|
||||
private readonly string _configPath;
|
||||
|
||||
public ConfigurationService(string startupDirectory)
|
||||
{
|
||||
_configPath = Path.Combine(startupDirectory, "config.json");
|
||||
}
|
||||
|
||||
public string ConfigPath
|
||||
{
|
||||
get { return _configPath; }
|
||||
}
|
||||
|
||||
public ConfigurationLoadResult Load()
|
||||
{
|
||||
if (!File.Exists(_configPath))
|
||||
{
|
||||
return new ConfigurationLoadResult
|
||||
{
|
||||
Config = CreateDefault(),
|
||||
IsMissing = true,
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var content = File.ReadAllText(_configPath, Encoding.UTF8);
|
||||
var config = JsonConvert.DeserializeObject<AppConfig>(content) ?? CreateDefault();
|
||||
return new ConfigurationLoadResult
|
||||
{
|
||||
Config = config,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ConfigurationLoadResult
|
||||
{
|
||||
Config = CreateDefault(),
|
||||
IsCorrupted = true,
|
||||
ErrorMessage = ex.Message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(AppConfig config)
|
||||
{
|
||||
config.SavedAt = DateTime.Now;
|
||||
var content = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
File.WriteAllText(_configPath, content, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
public AppConfig CreateDefault()
|
||||
{
|
||||
return new AppConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
592
DCIT.Core/Services/DiffSerializationService.cs
Normal file
592
DCIT.Core/Services/DiffSerializationService.cs
Normal file
@@ -0,0 +1,592 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using DCIT.Core.Models;
|
||||
using DCIT.Core.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class DiffSerializationService
|
||||
{
|
||||
private static readonly byte[] PayloadCiphertextPropertyMarker = Encoding.UTF8.GetBytes("\"PayloadCiphertext\":");
|
||||
private static readonly byte[] PayloadCiphertextValueMarker = Encoding.UTF8.GetBytes("\"PayloadCiphertext\":\"");
|
||||
private static readonly byte[] MetadataSyntheticSuffix = Encoding.UTF8.GetBytes("null,\"Payload\":null}");
|
||||
private const int MetadataScanCapBytes = 1 << 20;
|
||||
|
||||
private readonly BmpDiffCodec _bmpDiffCodec;
|
||||
private readonly string _appVersion;
|
||||
|
||||
public DiffSerializationService(BmpDiffCodec bmpDiffCodec)
|
||||
{
|
||||
_bmpDiffCodec = bmpDiffCodec;
|
||||
_appVersion = Assembly.GetExecutingAssembly().GetName().Version != null
|
||||
? Assembly.GetExecutingAssembly().GetName().Version.ToString()
|
||||
: "1.0.0";
|
||||
}
|
||||
|
||||
public string AppVersion
|
||||
{
|
||||
get { return _appVersion; }
|
||||
}
|
||||
|
||||
public string GetBmpPath(string diffPath)
|
||||
{
|
||||
return Path.ChangeExtension(diffPath, ".bmp");
|
||||
}
|
||||
|
||||
public string Write(DiffDocument plainDiff, string diffPath, bool encryptJson)
|
||||
{
|
||||
if (plainDiff == null)
|
||||
{
|
||||
throw new ArgumentNullException("plainDiff");
|
||||
}
|
||||
|
||||
if (plainDiff.Payload == null)
|
||||
{
|
||||
throw new InvalidOperationException("差异载荷不能为空。");
|
||||
}
|
||||
|
||||
var originalPayload = plainDiff.Payload;
|
||||
var originalEncrypted = plainDiff.Encrypted;
|
||||
var originalEncryption = plainDiff.Encryption;
|
||||
var originalPayloadCiphertext = plainDiff.PayloadCiphertext;
|
||||
|
||||
string payloadTmpPath = null;
|
||||
string cipherTmpPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
payloadTmpPath = Path.Combine(Path.GetTempPath(), "dcit-payload-" + Guid.NewGuid().ToString("N") + ".bin");
|
||||
SerializeInstanceToUtf8File(plainDiff.Payload, payloadTmpPath);
|
||||
plainDiff.Integrity.PayloadHash = HashUtility.ComputeFileSha256Base64(payloadTmpPath);
|
||||
plainDiff.Integrity.TamperProtectionValue = HashUtility.ComputeFileHmacBase64(payloadTmpPath);
|
||||
|
||||
plainDiff.Encrypted = false;
|
||||
plainDiff.Encryption = null;
|
||||
plainDiff.PayloadCiphertext = null;
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(diffPath) ?? string.Empty);
|
||||
var bmpPath = GetBmpPath(diffPath);
|
||||
|
||||
// Serialize the full plainDiff (with Payload attached) to BMP. Use a temp file so we
|
||||
// never hold the whole BMP-bound JSON in memory.
|
||||
var bmpTmpPath = Path.Combine(Path.GetTempPath(), "dcit-bmp-" + Guid.NewGuid().ToString("N") + ".json");
|
||||
try
|
||||
{
|
||||
SerializeInstanceToUtf8File(plainDiff, bmpTmpPath);
|
||||
using (var bmpFile = File.OpenRead(bmpTmpPath))
|
||||
{
|
||||
_bmpDiffCodec.EncodeFromStream(bmpFile, bmpPath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDeleteFile(bmpTmpPath);
|
||||
}
|
||||
|
||||
if (encryptJson)
|
||||
{
|
||||
cipherTmpPath = Path.Combine(Path.GetTempPath(), "dcit-cipher-" + Guid.NewGuid().ToString("N") + ".bin");
|
||||
EncryptedPayloadHeader header;
|
||||
using (var payloadStream = File.OpenRead(payloadTmpPath))
|
||||
using (var cipherStream = File.Create(cipherTmpPath))
|
||||
{
|
||||
header = HashUtility.EncryptStream(payloadStream, cipherStream);
|
||||
}
|
||||
|
||||
plainDiff.Encrypted = true;
|
||||
plainDiff.Encryption = new DiffEncryptionInfo
|
||||
{
|
||||
Algorithm = "AES-256-CBC+HMAC-SHA256",
|
||||
KeyId = "default",
|
||||
Nonce = header.NonceBase64,
|
||||
Tag = header.TagBase64,
|
||||
};
|
||||
plainDiff.PayloadCiphertext = null;
|
||||
plainDiff.Payload = null;
|
||||
|
||||
WriteEncryptedDiffToFile(plainDiff, diffPath, cipherTmpPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
SerializeToFile(plainDiff, diffPath);
|
||||
}
|
||||
|
||||
return bmpPath;
|
||||
}
|
||||
finally
|
||||
{
|
||||
plainDiff.Payload = originalPayload;
|
||||
plainDiff.Encrypted = originalEncrypted;
|
||||
plainDiff.Encryption = originalEncryption;
|
||||
plainDiff.PayloadCiphertext = originalPayloadCiphertext;
|
||||
|
||||
TryDeleteFile(payloadTmpPath);
|
||||
TryDeleteFile(cipherTmpPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SerializeInstanceToUtf8File(object instance, string path)
|
||||
{
|
||||
using (var stream = File.Create(path))
|
||||
using (var streamWriter = new StreamWriter(stream, new UTF8Encoding(false), bufferSize: 65536))
|
||||
using (var jsonWriter = new JsonTextWriter(streamWriter))
|
||||
{
|
||||
var serializer = JsonSerializer.CreateDefault();
|
||||
serializer.Formatting = Formatting.None;
|
||||
serializer.Serialize(jsonWriter, instance);
|
||||
jsonWriter.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteEncryptedDiffToFile(DiffDocument document, string diffPath, string cipherTempPath)
|
||||
{
|
||||
var metadataJson = JsonConvert.SerializeObject(document, Formatting.None);
|
||||
const string placeholder = "\"PayloadCiphertext\":null";
|
||||
var placeholderIndex = metadataJson.IndexOf(placeholder, StringComparison.Ordinal);
|
||||
if (placeholderIndex < 0)
|
||||
{
|
||||
throw new InvalidOperationException("加密差异元数据缺少 PayloadCiphertext 占位符。");
|
||||
}
|
||||
|
||||
using (var fileStream = File.Create(diffPath))
|
||||
using (var writer = new StreamWriter(fileStream, new UTF8Encoding(false), bufferSize: 65536))
|
||||
{
|
||||
writer.Write(metadataJson.Substring(0, placeholderIndex));
|
||||
writer.Write("\"PayloadCiphertext\":\"");
|
||||
using (var cipherStream = File.OpenRead(cipherTempPath))
|
||||
{
|
||||
var buffer = new byte[30720];
|
||||
int read;
|
||||
while ((read = cipherStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
writer.Write(Convert.ToBase64String(buffer, 0, read));
|
||||
}
|
||||
}
|
||||
|
||||
writer.Write('"');
|
||||
writer.Write(metadataJson.Substring(placeholderIndex + placeholder.Length));
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cleanup; temp files live in the system temp dir.
|
||||
}
|
||||
}
|
||||
|
||||
public LoadedDiffDocument Load(string diffOrBmpPath)
|
||||
{
|
||||
if (diffOrBmpPath == null)
|
||||
{
|
||||
throw new ArgumentNullException("diffOrBmpPath");
|
||||
}
|
||||
|
||||
if (Path.GetExtension(diffOrBmpPath).Equals(".bmp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LoadFromBmp(diffOrBmpPath);
|
||||
}
|
||||
|
||||
return LoadFromDiff(diffOrBmpPath);
|
||||
}
|
||||
|
||||
public DiffDocument ReadMetadata(string diffOrBmpPath)
|
||||
{
|
||||
if (diffOrBmpPath == null)
|
||||
{
|
||||
throw new ArgumentNullException("diffOrBmpPath");
|
||||
}
|
||||
|
||||
if (Path.GetExtension(diffOrBmpPath).Equals(".bmp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var json = _bmpDiffCodec.DecodeJson(diffOrBmpPath);
|
||||
return JsonConvert.DeserializeObject<DiffDocument>(json) ?? new DiffDocument();
|
||||
}
|
||||
|
||||
return ReadDiffMetadata(diffOrBmpPath);
|
||||
}
|
||||
|
||||
public DiffDocument CreateBaseDocument()
|
||||
{
|
||||
return new DiffDocument
|
||||
{
|
||||
App = new DiffAppInfo
|
||||
{
|
||||
Name = "DCIT",
|
||||
Version = _appVersion,
|
||||
},
|
||||
Algorithm = new DiffAlgorithmInfo
|
||||
{
|
||||
DiffVersion = "1.0",
|
||||
BmpCodecVersion = "1.0",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// BMP diffs use a legacy in-memory load path. Large BMP-encoded diffs remain a known
|
||||
// OOM risk; the default and reported path is .diff, which is fully streaming below.
|
||||
private LoadedDiffDocument LoadFromBmp(string bmpPath)
|
||||
{
|
||||
var json = _bmpDiffCodec.DecodeJson(bmpPath);
|
||||
var document = JsonConvert.DeserializeObject<DiffDocument>(json);
|
||||
if (document == null)
|
||||
{
|
||||
throw new InvalidDataException("差异文件解析失败。");
|
||||
}
|
||||
|
||||
if (document.Encrypted)
|
||||
{
|
||||
if (document.Encryption == null || string.IsNullOrWhiteSpace(document.PayloadCiphertext))
|
||||
{
|
||||
throw new InvalidDataException("加密差异文件缺少必要字段。");
|
||||
}
|
||||
|
||||
var payloadBytes = HashUtility.Decrypt(
|
||||
document.PayloadCiphertext,
|
||||
document.Encryption.Nonce,
|
||||
document.Encryption.Tag);
|
||||
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
|
||||
document.Payload = JsonConvert.DeserializeObject<DiffPayload>(payloadJson);
|
||||
ValidateIntegrity(document, payloadBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (document.Payload == null)
|
||||
{
|
||||
throw new InvalidDataException("明文差异文件缺少 payload。");
|
||||
}
|
||||
|
||||
ValidateIntegrity(document, Encoding.UTF8.GetBytes(SerializeCanonical(document.Payload)));
|
||||
}
|
||||
|
||||
return new LoadedDiffDocument
|
||||
{
|
||||
Document = document,
|
||||
};
|
||||
}
|
||||
|
||||
private LoadedDiffDocument LoadFromDiff(string diffPath)
|
||||
{
|
||||
var document = ReadDiffMetadata(diffPath);
|
||||
string payloadTmpPath = null;
|
||||
string cipherTmpPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (document.Encrypted)
|
||||
{
|
||||
if (document.Encryption == null
|
||||
|| string.IsNullOrWhiteSpace(document.Encryption.Nonce)
|
||||
|| string.IsNullOrWhiteSpace(document.Encryption.Tag))
|
||||
{
|
||||
throw new InvalidDataException("加密差异文件缺少加密元数据。");
|
||||
}
|
||||
|
||||
cipherTmpPath = Path.Combine(Path.GetTempPath(), "dcit-rcipher-" + Guid.NewGuid().ToString("N") + ".bin");
|
||||
payloadTmpPath = Path.Combine(Path.GetTempPath(), "dcit-rpayload-" + Guid.NewGuid().ToString("N") + ".bin");
|
||||
|
||||
ExtractBase64RegionToFile(diffPath, PayloadCiphertextValueMarker, cipherTmpPath);
|
||||
|
||||
var iv = Convert.FromBase64String(document.Encryption.Nonce);
|
||||
var tag = Convert.FromBase64String(document.Encryption.Tag);
|
||||
using (var cipherStream = File.OpenRead(cipherTmpPath))
|
||||
using (var payloadStream = File.Create(payloadTmpPath))
|
||||
{
|
||||
HashUtility.DecryptStream(cipherStream, payloadStream, iv, tag);
|
||||
}
|
||||
|
||||
ValidateIntegrityFromPayloadFile(document, payloadTmpPath);
|
||||
document.Payload = DeserializePayloadFromFile(payloadTmpPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stream-deserialize the whole document. PayloadCiphertext is null (small) and
|
||||
// Payload is a large nested object that Newtonsoft builds incrementally without
|
||||
// buffering the whole JSON text or any single huge string token.
|
||||
var full = StreamDeserializeDiffDocument(diffPath);
|
||||
if (full == null)
|
||||
{
|
||||
throw new InvalidDataException("差异文件解析失败。");
|
||||
}
|
||||
|
||||
if (full.Payload == null)
|
||||
{
|
||||
throw new InvalidDataException("明文差异文件缺少 payload。");
|
||||
}
|
||||
|
||||
document = full;
|
||||
payloadTmpPath = Path.Combine(Path.GetTempPath(), "dcit-rpayload-" + Guid.NewGuid().ToString("N") + ".bin");
|
||||
SerializeInstanceToUtf8File(document.Payload, payloadTmpPath);
|
||||
ValidateIntegrityFromPayloadFile(document, payloadTmpPath);
|
||||
}
|
||||
|
||||
return new LoadedDiffDocument
|
||||
{
|
||||
Document = document,
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDeleteFile(payloadTmpPath);
|
||||
TryDeleteFile(cipherTmpPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Reads only the metadata portion of a .diff file. The huge fields (PayloadCiphertext value
|
||||
// and Payload value) sit after the metadata in declaration order; we byte-scan up to the
|
||||
// "PayloadCiphertext" property marker (a few KB), synthesize null values for the two huge
|
||||
// fields, and parse the resulting small metadata JSON. The huge content is never read.
|
||||
private DiffDocument ReadDiffMetadata(string diffPath)
|
||||
{
|
||||
var prefix = ReadPrefixThroughMarker(diffPath, PayloadCiphertextPropertyMarker, MetadataScanCapBytes);
|
||||
string metadataJson;
|
||||
if (prefix == null)
|
||||
{
|
||||
metadataJson = File.ReadAllText(diffPath, Encoding.UTF8);
|
||||
}
|
||||
else
|
||||
{
|
||||
var combined = new byte[prefix.Length + MetadataSyntheticSuffix.Length];
|
||||
Buffer.BlockCopy(prefix, 0, combined, 0, prefix.Length);
|
||||
Buffer.BlockCopy(MetadataSyntheticSuffix, 0, combined, prefix.Length, MetadataSyntheticSuffix.Length);
|
||||
metadataJson = Encoding.UTF8.GetString(combined);
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<DiffDocument>(metadataJson) ?? new DiffDocument();
|
||||
}
|
||||
|
||||
private static DiffDocument StreamDeserializeDiffDocument(string diffPath)
|
||||
{
|
||||
using (var stream = File.OpenRead(diffPath))
|
||||
using (var streamReader = new StreamReader(stream, new UTF8Encoding(false)))
|
||||
using (var jsonReader = new JsonTextReader(streamReader))
|
||||
{
|
||||
return JsonSerializer.CreateDefault().Deserialize<DiffDocument>(jsonReader);
|
||||
}
|
||||
}
|
||||
|
||||
private static DiffPayload DeserializePayloadFromFile(string payloadPath)
|
||||
{
|
||||
using (var stream = File.OpenRead(payloadPath))
|
||||
using (var streamReader = new StreamReader(stream, new UTF8Encoding(false)))
|
||||
using (var jsonReader = new JsonTextReader(streamReader))
|
||||
{
|
||||
return JsonSerializer.CreateDefault().Deserialize<DiffPayload>(jsonReader);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateIntegrity(DiffDocument document, byte[] payloadBytes)
|
||||
{
|
||||
var actualHash = HashUtility.ComputeSha256Base64(payloadBytes);
|
||||
var actualTamper = HashUtility.ComputeHmacBase64(payloadBytes);
|
||||
if (!string.Equals(actualHash, document.Integrity == null ? null : document.Integrity.PayloadHash, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException("差异文件载荷哈希校验失败。");
|
||||
}
|
||||
|
||||
if (!string.Equals(actualTamper, document.Integrity == null ? null : document.Integrity.TamperProtectionValue, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException("差异文件防篡改校验失败。");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateIntegrityFromPayloadFile(DiffDocument document, string payloadPath)
|
||||
{
|
||||
var actualHash = HashUtility.ComputeFileSha256Base64(payloadPath);
|
||||
var actualTamper = HashUtility.ComputeFileHmacBase64(payloadPath);
|
||||
if (!string.Equals(actualHash, document.Integrity == null ? null : document.Integrity.PayloadHash, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException("差异文件载荷哈希校验失败。");
|
||||
}
|
||||
|
||||
if (!string.Equals(actualTamper, document.Integrity == null ? null : document.Integrity.TamperProtectionValue, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException("差异文件防篡改校验失败。");
|
||||
}
|
||||
}
|
||||
|
||||
private static string SerializeCanonical(object instance)
|
||||
{
|
||||
return JsonConvert.SerializeObject(instance, Formatting.None);
|
||||
}
|
||||
|
||||
private static void SerializeToFile(object instance, string path)
|
||||
{
|
||||
using (var stream = File.Create(path))
|
||||
using (var streamWriter = new StreamWriter(stream, new UTF8Encoding(false), bufferSize: 65536))
|
||||
using (var jsonWriter = new JsonTextWriter(streamWriter))
|
||||
{
|
||||
var serializer = JsonSerializer.CreateDefault();
|
||||
serializer.Formatting = Formatting.None;
|
||||
serializer.Serialize(jsonWriter, instance);
|
||||
jsonWriter.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Reads bytes from the start of the file up to and including the first occurrence of marker.
|
||||
// Returns null if the marker is not found within cap bytes. Caller chooses cap so the marker
|
||||
// (which sits after the small metadata block) is found without reading huge payload content.
|
||||
private static byte[] ReadPrefixThroughMarker(string path, byte[] marker, int cap)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
var buffer = new byte[65536];
|
||||
int read;
|
||||
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
ms.Write(buffer, 0, read);
|
||||
if (ms.Length >= cap)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var arr = ms.ToArray();
|
||||
var idx = IndexOfSubarray(arr, marker);
|
||||
if (idx < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new byte[idx + marker.Length];
|
||||
Buffer.BlockCopy(arr, 0, result, 0, result.Length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Locates valueMarker in the file, then stream-decodes the base64 region between the marker
|
||||
// and the next closing double-quote into outputPath. Decodes in 4096-char chunks (a multiple
|
||||
// of 4) so there is no mid-chunk base64 alignment error.
|
||||
private static void ExtractBase64RegionToFile(string diffPath, byte[] valueMarker, string outputPath)
|
||||
{
|
||||
long markerEndPosition;
|
||||
using (var probe = File.OpenRead(diffPath))
|
||||
{
|
||||
markerEndPosition = FindMarkerEnd(probe, valueMarker);
|
||||
}
|
||||
|
||||
if (markerEndPosition < 0)
|
||||
{
|
||||
throw new InvalidDataException("差异文件缺少密文字段标记。");
|
||||
}
|
||||
|
||||
using (var stream = File.OpenRead(diffPath))
|
||||
{
|
||||
stream.Position = markerEndPosition;
|
||||
using (var outStream = File.Create(outputPath))
|
||||
{
|
||||
var charBuf = new char[4096];
|
||||
var charLen = 0;
|
||||
var byteBuf = new byte[65536];
|
||||
int read;
|
||||
var done = false;
|
||||
while (!done && (read = stream.Read(byteBuf, 0, byteBuf.Length)) > 0)
|
||||
{
|
||||
for (var i = 0; i < read; i++)
|
||||
{
|
||||
var b = byteBuf[i];
|
||||
if (b == '"')
|
||||
{
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
|
||||
charBuf[charLen++] = (char)b;
|
||||
if (charLen == charBuf.Length)
|
||||
{
|
||||
var decoded = Convert.FromBase64CharArray(charBuf, 0, charLen);
|
||||
outStream.Write(decoded, 0, decoded.Length);
|
||||
charLen = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (charLen > 0)
|
||||
{
|
||||
var decoded = Convert.FromBase64CharArray(charBuf, 0, charLen);
|
||||
outStream.Write(decoded, 0, decoded.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long FindMarkerEnd(Stream stream, byte[] marker)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
var buffer = new byte[65536];
|
||||
int read;
|
||||
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
ms.Write(buffer, 0, read);
|
||||
var arr = ms.ToArray();
|
||||
var idx = IndexOfSubarray(arr, marker);
|
||||
if (idx >= 0)
|
||||
{
|
||||
return idx + marker.Length;
|
||||
}
|
||||
|
||||
if (ms.Length > MetadataScanCapBytes)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int IndexOfSubarray(byte[] haystack, byte[] needle)
|
||||
{
|
||||
if (needle == null || needle.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (haystack == null || haystack.Length < needle.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (var i = 0; i <= haystack.Length - needle.Length; i++)
|
||||
{
|
||||
var match = true;
|
||||
for (var j = 0; j < needle.Length; j++)
|
||||
{
|
||||
if (haystack[i + j] != needle[j])
|
||||
{
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public class LoadedDiffDocument
|
||||
{
|
||||
public DiffDocument Document { get; set; }
|
||||
}
|
||||
}
|
||||
4434
DCIT.Core/Services/DocumentProcessingService.cs
Normal file
4434
DCIT.Core/Services/DocumentProcessingService.cs
Normal file
File diff suppressed because it is too large
Load Diff
341
DCIT.Core/Services/FileScanService.cs
Normal file
341
DCIT.Core/Services/FileScanService.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using DCIT.Core.Models;
|
||||
using DCIT.Core.Utilities;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class FileScanService
|
||||
{
|
||||
private static readonly TimeSpan RuleMatchTimeout = TimeSpan.FromSeconds(1);
|
||||
private readonly DiffSerializationService _diffSerializationService;
|
||||
|
||||
public FileScanService()
|
||||
: this(new DiffSerializationService(new BmpDiffCodec()))
|
||||
{
|
||||
}
|
||||
|
||||
public FileScanService(DiffSerializationService diffSerializationService)
|
||||
{
|
||||
if (diffSerializationService == null)
|
||||
{
|
||||
throw new ArgumentNullException("diffSerializationService");
|
||||
}
|
||||
|
||||
_diffSerializationService = diffSerializationService;
|
||||
}
|
||||
|
||||
public IList<FileScanItem> ScanReplaceCandidates(string sourceDirectory, string outputDirectory)
|
||||
{
|
||||
return ScanReplaceCandidates(sourceDirectory, outputDirectory, null);
|
||||
}
|
||||
|
||||
public IList<FileScanItem> ScanReplaceCandidates(string sourceDirectory, string outputDirectory, RuleFile ruleFile)
|
||||
{
|
||||
var items = new List<FileScanItem>();
|
||||
var hasOutputDirectory = !string.IsNullOrWhiteSpace(outputDirectory);
|
||||
var rules = GetEnabledRules(ruleFile, RuleTarget.FileName);
|
||||
var files = EnumerateWordFiles(sourceDirectory);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var relativePath = PathUtility.GetRelativePath(sourceDirectory, file);
|
||||
var relativeDirectory = Path.GetDirectoryName(relativePath) ?? string.Empty;
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
|
||||
var extension = Path.GetExtension(file);
|
||||
var replacedFileNameWithoutExtension = ApplyRulesToFileName(fileNameWithoutExtension, rules);
|
||||
ValidateReplacementFileName(file, replacedFileNameWithoutExtension);
|
||||
|
||||
var datedFileName = string.Format("{0}-{1:yyyyMMdd}{2}", replacedFileNameWithoutExtension, DateTime.Today, extension);
|
||||
var outputPath = hasOutputDirectory
|
||||
? Path.Combine(outputDirectory, relativeDirectory, datedFileName)
|
||||
: string.Empty;
|
||||
var diffPath = hasOutputDirectory
|
||||
? Path.Combine(outputDirectory, relativeDirectory, string.Format("{0}-{1:yyyyMMdd}.diff", replacedFileNameWithoutExtension, DateTime.Today))
|
||||
: string.Empty;
|
||||
|
||||
items.Add(new FileScanItem
|
||||
{
|
||||
IsSelected = true,
|
||||
SourcePath = file,
|
||||
OutputPath = outputPath,
|
||||
DiffPath = diffPath,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public IList<FileScanItem> ScanRestoreCandidates(string replaceDirectory, string restoreDirectory, string diffInputFormat)
|
||||
{
|
||||
var hasRestoreDirectory = !string.IsNullOrWhiteSpace(restoreDirectory);
|
||||
var normalizedFormat = string.Equals(diffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase)
|
||||
? ".bmp"
|
||||
: ".diff";
|
||||
var selectedDiffs = Directory.Exists(replaceDirectory)
|
||||
? Directory.EnumerateFiles(replaceDirectory, "*" + normalizedFormat, SearchOption.AllDirectories).ToList()
|
||||
: new List<string>();
|
||||
var diffLookup = selectedDiffs.ToDictionary(
|
||||
path => BuildRestoreKey(replaceDirectory, path),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var wordFiles = EnumerateWordFiles(replaceDirectory);
|
||||
var items = new List<FileScanItem>();
|
||||
foreach (var wordFile in wordFiles)
|
||||
{
|
||||
var key = BuildRestoreKey(replaceDirectory, wordFile);
|
||||
if (!diffLookup.ContainsKey(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relativePath = PathUtility.GetRelativePath(replaceDirectory, wordFile);
|
||||
var relativeDirectory = Path.GetDirectoryName(relativePath) ?? string.Empty;
|
||||
var extension = Path.GetExtension(wordFile);
|
||||
var restoreFileName = ResolveRestoreFileName(diffLookup[key], wordFile);
|
||||
var restorePath = hasRestoreDirectory
|
||||
? Path.Combine(restoreDirectory, relativeDirectory, restoreFileName ?? Path.GetFileNameWithoutExtension(wordFile) + "-rcv" + extension)
|
||||
: string.Empty;
|
||||
|
||||
items.Add(new FileScanItem
|
||||
{
|
||||
IsSelected = true,
|
||||
SourcePath = wordFile,
|
||||
OutputPath = restorePath,
|
||||
DiffPath = diffLookup[key],
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public IList<string> ValidateReplaceDirectories(string sourceDirectory, string outputDirectory)
|
||||
{
|
||||
return PathUtility.ValidateDirectoryPair("原始文件夹", sourceDirectory, "替换文件夹", outputDirectory).ToList();
|
||||
}
|
||||
|
||||
public IList<string> ValidateRestoreDirectories(string replaceDirectory, string restoreDirectory)
|
||||
{
|
||||
return PathUtility.ValidateDirectoryPair("替换文件夹", replaceDirectory, "恢复文件夹", restoreDirectory).ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateWordFiles(string rootDirectory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
return Directory.EnumerateFiles(rootDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.Where(path =>
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
if (fileName != null && fileName.StartsWith("~$", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
return extension.Equals(".doc", StringComparison.OrdinalIgnoreCase)
|
||||
|| extension.Equals(".docx", StringComparison.OrdinalIgnoreCase);
|
||||
})
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private string ResolveRestoreFileName(string diffPath, string wordFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = _diffSerializationService.ReadMetadata(diffPath);
|
||||
var sourceFileName = document == null || document.SourceDocument == null
|
||||
? null
|
||||
: document.SourceDocument.FileName;
|
||||
if (!string.IsNullOrWhiteSpace(sourceFileName))
|
||||
{
|
||||
var safeFileName = Path.GetFileName(sourceFileName);
|
||||
if (!string.IsNullOrWhiteSpace(safeFileName)
|
||||
&& string.Equals(safeFileName, sourceFileName, StringComparison.Ordinal)
|
||||
&& !ContainsInvalidFileNameCharacter(safeFileName))
|
||||
{
|
||||
return safeFileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Legacy or placeholder diff files are still paired by basename and use the old preview naming.
|
||||
}
|
||||
|
||||
return Path.GetFileNameWithoutExtension(wordFile) + "-rcv" + Path.GetExtension(wordFile);
|
||||
}
|
||||
|
||||
private static IList<RuleDefinition> GetEnabledRules(RuleFile ruleFile, RuleTarget target)
|
||||
{
|
||||
if (ruleFile == null || ruleFile.Rules == null)
|
||||
{
|
||||
return new List<RuleDefinition>();
|
||||
}
|
||||
|
||||
return ruleFile.Rules
|
||||
.Where(rule => rule != null && rule.IsEnabled && !string.IsNullOrEmpty(rule.SearchText) && rule.Target == target)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static string ApplyRulesToFileName(string fileNameWithoutExtension, IList<RuleDefinition> rules)
|
||||
{
|
||||
var result = fileNameWithoutExtension ?? string.Empty;
|
||||
if (rules == null || rules.Count == 0 || string.IsNullOrEmpty(result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
result = ApplyRuleToFileName(result, rule);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ApplyRuleToFileName(string text, RuleDefinition rule)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || rule == null || string.IsNullOrEmpty(rule.SearchText))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
if (rule.MatchMode == RuleMatchMode.RegularExpression)
|
||||
{
|
||||
var regex = new Regex(rule.SearchText, GetRegexOptions(rule), RuleMatchTimeout);
|
||||
return regex.Replace(text, match =>
|
||||
{
|
||||
if (!match.Success || match.Length <= 0)
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
return match.Result(rule.ReplacementText ?? string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
var escapedPattern = Regex.Escape(rule.SearchText);
|
||||
var plainRegex = new Regex(escapedPattern, GetRegexOptions(rule), RuleMatchTimeout);
|
||||
return plainRegex.Replace(text, match => rule.ReplacementText ?? string.Empty);
|
||||
}
|
||||
|
||||
private static string ApplyRuleToText(string text, RuleDefinition rule)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || rule == null || string.IsNullOrEmpty(rule.SearchText))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
if (rule.MatchMode == RuleMatchMode.RegularExpression)
|
||||
{
|
||||
var regex = new Regex(rule.SearchText, GetRegexOptions(rule), RuleMatchTimeout);
|
||||
return regex.Replace(text, match =>
|
||||
{
|
||||
if (!match.Success || match.Length <= 0)
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
if (rule.IsWholeWord && !IsWholeWord(text, match.Index, match.Length))
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
return match.Result(rule.ReplacementText ?? string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
var escapedPattern = Regex.Escape(rule.SearchText);
|
||||
var plainRegex = new Regex(escapedPattern, GetRegexOptions(rule), RuleMatchTimeout);
|
||||
return plainRegex.Replace(text, match =>
|
||||
{
|
||||
if (rule.IsWholeWord && !IsWholeWord(text, match.Index, match.Length))
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
return rule.ReplacementText ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private static RegexOptions GetRegexOptions(RuleDefinition rule)
|
||||
{
|
||||
var options = RegexOptions.CultureInvariant;
|
||||
if (!rule.IsCaseSensitive)
|
||||
{
|
||||
options |= RegexOptions.IgnoreCase;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static bool IsWholeWord(string text, int start, int length)
|
||||
{
|
||||
var leftOk = start <= 0 || GetWordCharClass(text[start - 1]) != GetWordCharClass(text[start]);
|
||||
var rightIndex = start + length;
|
||||
var rightOk = rightIndex >= text.Length || GetWordCharClass(text[rightIndex - 1]) != GetWordCharClass(text[rightIndex]);
|
||||
return leftOk && rightOk;
|
||||
}
|
||||
|
||||
private static int GetWordCharClass(char character)
|
||||
{
|
||||
if ((character >= 'a' && character <= 'z')
|
||||
|| (character >= 'A' && character <= 'Z')
|
||||
|| (character >= '0' && character <= '9')
|
||||
|| character == '_')
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (character >= 0x4E00 && character <= 0x9FFF)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void ValidateReplacementFileName(string sourcePath, string fileNameWithoutExtension)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileNameWithoutExtension))
|
||||
{
|
||||
throw new InvalidDataException("文件名规则替换结果为空,无法生成输出文件名。源文件: " + sourcePath);
|
||||
}
|
||||
|
||||
if (ContainsInvalidFileNameCharacter(fileNameWithoutExtension))
|
||||
{
|
||||
throw new InvalidDataException("文件名规则替换结果包含非法文件名字符,无法生成输出文件名。源文件: "
|
||||
+ sourcePath
|
||||
+ ";替换后文件名: "
|
||||
+ fileNameWithoutExtension);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsInvalidFileNameCharacter(string fileName)
|
||||
{
|
||||
if (fileName == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
|
||||
|| fileName.IndexOf(Path.DirectorySeparatorChar) >= 0
|
||||
|| fileName.IndexOf(Path.AltDirectorySeparatorChar) >= 0;
|
||||
}
|
||||
|
||||
private static string BuildRestoreKey(string rootDirectory, string path)
|
||||
{
|
||||
var relativePath = PathUtility.GetRelativePath(rootDirectory, path);
|
||||
var directory = Path.GetDirectoryName(relativePath) ?? string.Empty;
|
||||
var fileName = Path.GetFileNameWithoutExtension(relativePath);
|
||||
return Path.Combine(directory, fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
908
DCIT.Core/Services/FontValidationService.cs
Normal file
908
DCIT.Core/Services/FontValidationService.cs
Normal file
@@ -0,0 +1,908 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing.Text;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using W = DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public sealed class FontValidationService : IFontValidationService
|
||||
{
|
||||
private const string WordprocessingNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
private const string DefaultEastAsiaFallbackFont = "宋体";
|
||||
private const string DefaultLatinFallbackFont = "Times New Roman";
|
||||
|
||||
private static readonly string[] ThemeFontTokens =
|
||||
{
|
||||
"majorAscii",
|
||||
"majorHAnsi",
|
||||
"majorEastAsia",
|
||||
"majorBidi",
|
||||
"minorAscii",
|
||||
"minorHAnsi",
|
||||
"minorEastAsia",
|
||||
"minorBidi",
|
||||
};
|
||||
|
||||
private static readonly string[][] KnownFontAliasGroups =
|
||||
{
|
||||
new[] { "宋体", "SimSun" },
|
||||
new[] { "新宋体", "NSimSun" },
|
||||
new[] { "黑体", "SimHei" },
|
||||
new[] { "仿宋", "FangSong" },
|
||||
new[] { "楷体", "KaiTi" },
|
||||
new[] { "微软雅黑", "Microsoft YaHei" },
|
||||
new[] { "微软雅黑 Light", "Microsoft YaHei Light" },
|
||||
new[] { "微软雅黑 Bold", "Microsoft YaHei Bold" },
|
||||
new[] { "等线", "DengXian" },
|
||||
new[] { "等线 Light", "DengXian Light" },
|
||||
new[] { "等线 Bold", "DengXian Bold" },
|
||||
new[] { "华文宋体", "STSong" },
|
||||
new[] { "华文黑体", "STHeiti" },
|
||||
new[] { "华文中宋", "STZhongsong" },
|
||||
new[] { "华文仿宋", "STFangsong" },
|
||||
new[] { "华文楷体", "STKaiti" },
|
||||
new[] { "幼圆", "YouYuan" },
|
||||
new[] { "隶书", "LiSu" },
|
||||
};
|
||||
|
||||
private static readonly Lazy<Dictionary<string, HashSet<string>>> KnownFontAliases =
|
||||
new Lazy<Dictionary<string, HashSet<string>>>(BuildKnownFontAliases);
|
||||
|
||||
private readonly Lazy<HashSet<string>> _installedFonts;
|
||||
|
||||
public FontValidationService()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public FontValidationService(IEnumerable<string> installedFonts)
|
||||
{
|
||||
_installedFonts = new Lazy<HashSet<string>>(
|
||||
delegate
|
||||
{
|
||||
return installedFonts == null
|
||||
? LoadInstalledFonts()
|
||||
: new HashSet<string>(
|
||||
installedFonts
|
||||
.Select(NormalizeFontName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
public FontValidationResult Validate(string docxPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(docxPath))
|
||||
{
|
||||
throw new ArgumentException("docxPath");
|
||||
}
|
||||
|
||||
if (!File.Exists(docxPath))
|
||||
{
|
||||
throw new FileNotFoundException("未找到待校验的文档。", docxPath);
|
||||
}
|
||||
|
||||
using (var document = WordprocessingDocument.Open(docxPath, false))
|
||||
{
|
||||
var themeFonts = BuildThemeFontMap(document);
|
||||
var styleIndex = BuildStyleIndex(document);
|
||||
var defaultFonts = ReadDocumentDefaultFonts(document, themeFonts);
|
||||
var documentFontAliases = BuildDocumentFontAliasMap(document);
|
||||
var usedFonts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var root in EnumerateRelevantRoots(document))
|
||||
{
|
||||
CollectWordRunFonts(root, styleIndex, defaultFonts, themeFonts, usedFonts);
|
||||
CollectDrawingFonts(root, usedFonts);
|
||||
}
|
||||
|
||||
var missingFonts = usedFonts
|
||||
.Select(NormalizeFontName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Where(name => !IsFontAvailable(name, documentFontAliases))
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return new FontValidationResult(
|
||||
usedFonts.OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToList(),
|
||||
missingFonts);
|
||||
}
|
||||
}
|
||||
|
||||
public FontValidationResult EnsureFontsAvailable(string docxPath)
|
||||
{
|
||||
var result = Validate(docxPath);
|
||||
if (result.MissingFonts.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
ApplyMissingFontFallbacks(docxPath, result.MissingFonts);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ApplyMissingFontFallbacks(string docxPath, IEnumerable<string> missingFonts)
|
||||
{
|
||||
var missingFontSet = new HashSet<string>(
|
||||
(missingFonts ?? Array.Empty<string>())
|
||||
.Select(NormalizeFontName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
if (missingFontSet.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var document = WordprocessingDocument.Open(docxPath, true))
|
||||
{
|
||||
var themeFonts = BuildThemeFontMap(document);
|
||||
foreach (var root in EnumerateFontFallbackRoots(document))
|
||||
{
|
||||
var changed = false;
|
||||
foreach (var runFonts in root.Descendants<W.RunFonts>())
|
||||
{
|
||||
changed |= ApplyWordRunFontFallbacks(runFonts, missingFontSet, themeFonts);
|
||||
}
|
||||
|
||||
foreach (var element in root.Descendants())
|
||||
{
|
||||
changed |= ApplyDrawingFontFallback(element, missingFontSet);
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
root.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<OpenXmlPartRootElement> EnumerateFontFallbackRoots(WordprocessingDocument document)
|
||||
{
|
||||
var main = document.MainDocumentPart;
|
||||
if (main == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (main.Document != null)
|
||||
{
|
||||
yield return main.Document;
|
||||
}
|
||||
|
||||
var queue = new Queue<OpenXmlPartContainer>();
|
||||
var visited = new HashSet<OpenXmlPart>();
|
||||
queue.Enqueue(main);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var container = queue.Dequeue();
|
||||
foreach (var pair in container.Parts)
|
||||
{
|
||||
var part = pair.OpenXmlPart;
|
||||
if (part == null || !visited.Add(part))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.RootElement != null)
|
||||
{
|
||||
yield return part.RootElement;
|
||||
}
|
||||
|
||||
queue.Enqueue(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ApplyWordRunFontFallbacks(
|
||||
W.RunFonts runFonts,
|
||||
ISet<string> missingFonts,
|
||||
IDictionary<string, string> themeFonts)
|
||||
{
|
||||
var changed = false;
|
||||
changed |= ApplyWordFontFallback(
|
||||
runFonts,
|
||||
"ascii",
|
||||
new[] { "asciiTheme" },
|
||||
DefaultLatinFallbackFont,
|
||||
missingFonts,
|
||||
themeFonts);
|
||||
changed |= ApplyWordFontFallback(
|
||||
runFonts,
|
||||
"hAnsi",
|
||||
new[] { "hAnsiTheme" },
|
||||
DefaultLatinFallbackFont,
|
||||
missingFonts,
|
||||
themeFonts);
|
||||
changed |= ApplyWordFontFallback(
|
||||
runFonts,
|
||||
"eastAsia",
|
||||
new[] { "eastAsiaTheme" },
|
||||
DefaultEastAsiaFallbackFont,
|
||||
missingFonts,
|
||||
themeFonts);
|
||||
changed |= ApplyWordFontFallback(
|
||||
runFonts,
|
||||
"cs",
|
||||
new[] { "csTheme", "cstheme" },
|
||||
DefaultLatinFallbackFont,
|
||||
missingFonts,
|
||||
themeFonts);
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool ApplyWordFontFallback(
|
||||
OpenXmlElement element,
|
||||
string directLocalName,
|
||||
IEnumerable<string> themeLocalNames,
|
||||
string fallbackFont,
|
||||
ISet<string> missingFonts,
|
||||
IDictionary<string, string> themeFonts)
|
||||
{
|
||||
var directFont = NormalizeFontName(GetAttributeValue(element, directLocalName));
|
||||
var resolvedFont = directFont;
|
||||
if (string.IsNullOrWhiteSpace(resolvedFont))
|
||||
{
|
||||
foreach (var themeLocalName in themeLocalNames)
|
||||
{
|
||||
resolvedFont = ResolveFontName(null, GetAttributeValue(element, themeLocalName), themeFonts);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedFont))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(resolvedFont) || !missingFonts.Contains(resolvedFont))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
if (!string.Equals(directFont, fallbackFont, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SetWordprocessingAttribute(element, directLocalName, fallbackFont);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed |= RemoveAttributesByLocalName(element, themeLocalNames);
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool ApplyDrawingFontFallback(OpenXmlElement element, ISet<string> missingFonts)
|
||||
{
|
||||
if (!(string.Equals(element.LocalName, "latin", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(element.LocalName, "cs", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeface = NormalizeFontName(GetAttributeValue(element, "typeface"));
|
||||
if (string.IsNullOrWhiteSpace(typeface) || !missingFonts.Contains(typeface))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fallbackFont = ResolveDrawingFallbackFont(element);
|
||||
if (string.Equals(typeface, fallbackFont, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
element.SetAttribute(new OpenXmlAttribute(string.Empty, "typeface", string.Empty, fallbackFont));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ResolveDrawingFallbackFont(OpenXmlElement element)
|
||||
{
|
||||
if (string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return DefaultEastAsiaFallbackFont;
|
||||
}
|
||||
|
||||
if (string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase)
|
||||
&& IsEastAsiaScript(GetAttributeValue(element, "script")))
|
||||
{
|
||||
return DefaultEastAsiaFallbackFont;
|
||||
}
|
||||
|
||||
return DefaultLatinFallbackFont;
|
||||
}
|
||||
|
||||
private static bool IsEastAsiaScript(string script)
|
||||
{
|
||||
var normalized = NormalizeFontName(script);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalized.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| normalized.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| normalized.IndexOf("Jpan", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| normalized.IndexOf("Hang", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| normalized.IndexOf("Kore", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| normalized.IndexOf("Bopo", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static void SetWordprocessingAttribute(OpenXmlElement element, string localName, string value)
|
||||
{
|
||||
element.SetAttribute(new OpenXmlAttribute("w", localName, WordprocessingNamespace, value));
|
||||
}
|
||||
|
||||
private static bool RemoveAttributesByLocalName(OpenXmlElement element, IEnumerable<string> localNames)
|
||||
{
|
||||
var names = new HashSet<string>(
|
||||
(localNames ?? Array.Empty<string>()).Where(name => !string.IsNullOrWhiteSpace(name)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
if (names.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
foreach (var attribute in element.GetAttributes().Where(attribute => names.Contains(attribute.LocalName)).ToList())
|
||||
{
|
||||
element.RemoveAttribute(attribute.LocalName, attribute.NamespaceUri);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static HashSet<string> LoadInstalledFonts()
|
||||
{
|
||||
var collection = new InstalledFontCollection();
|
||||
return new HashSet<string>(
|
||||
collection.Families
|
||||
.Select(item => NormalizeFontName(item.Name))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static Dictionary<string, HashSet<string>> BuildDocumentFontAliasMap(WordprocessingDocument document)
|
||||
{
|
||||
var result = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
var fontTable = document.MainDocumentPart == null || document.MainDocumentPart.FontTablePart == null
|
||||
? null
|
||||
: document.MainDocumentPart.FontTablePart.Fonts;
|
||||
if (fontTable == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var font in fontTable.Elements<W.Font>())
|
||||
{
|
||||
var primary = NormalizeFontName(font.Name == null ? null : font.Name.Value);
|
||||
var alternate = NormalizeFontName(font.AltName == null || font.AltName.Val == null ? null : font.AltName.Val.Value);
|
||||
AddAliasPair(result, primary, alternate);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<OpenXmlPartRootElement> EnumerateRelevantRoots(WordprocessingDocument document)
|
||||
{
|
||||
var main = document.MainDocumentPart;
|
||||
if (main == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (main.Document != null)
|
||||
{
|
||||
yield return main.Document;
|
||||
}
|
||||
|
||||
var queue = new Queue<OpenXmlPartContainer>();
|
||||
var visited = new HashSet<OpenXmlPart>();
|
||||
queue.Enqueue(main);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var container = queue.Dequeue();
|
||||
foreach (var pair in container.Parts)
|
||||
{
|
||||
var part = pair.OpenXmlPart;
|
||||
if (part == null || !visited.Add(part))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.RootElement != null && IsRelevantPart(part))
|
||||
{
|
||||
yield return part.RootElement;
|
||||
}
|
||||
|
||||
queue.Enqueue(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRelevantPart(OpenXmlPart part)
|
||||
{
|
||||
return part is HeaderPart
|
||||
|| part is FooterPart
|
||||
|| part is WordprocessingCommentsPart
|
||||
|| part is FootnotesPart
|
||||
|| part is EndnotesPart
|
||||
|| part.GetType().Name.IndexOf("ChartPart", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static Dictionary<string, StyleFontSet> BuildStyleIndex(WordprocessingDocument document)
|
||||
{
|
||||
var result = new Dictionary<string, StyleFontSet>(StringComparer.OrdinalIgnoreCase);
|
||||
var styles = document.MainDocumentPart == null || document.MainDocumentPart.StyleDefinitionsPart == null
|
||||
? null
|
||||
: document.MainDocumentPart.StyleDefinitionsPart.Styles;
|
||||
|
||||
if (styles == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var style in styles.Elements<W.Style>())
|
||||
{
|
||||
var styleId = style.StyleId == null ? null : style.StyleId.Value;
|
||||
if (string.IsNullOrWhiteSpace(styleId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result[styleId] = new StyleFontSet
|
||||
{
|
||||
StyleId = styleId,
|
||||
BasedOnStyleId = style.BasedOn == null ? null : style.BasedOn.Val == null ? null : style.BasedOn.Val.Value,
|
||||
Fonts = ExtractWordFontSet(style.StyleRunProperties == null ? null : style.StyleRunProperties.RunFonts, null),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static FontReferenceSet ReadDocumentDefaultFonts(WordprocessingDocument document, IDictionary<string, string> themeFonts)
|
||||
{
|
||||
var styles = document.MainDocumentPart == null || document.MainDocumentPart.StyleDefinitionsPart == null
|
||||
? null
|
||||
: document.MainDocumentPart.StyleDefinitionsPart.Styles;
|
||||
var docDefaults = styles == null ? null : styles.DocDefaults;
|
||||
var runDefaults = docDefaults == null ? null : docDefaults.RunPropertiesDefault;
|
||||
var baseStyle = runDefaults == null ? null : runDefaults.RunPropertiesBaseStyle;
|
||||
return ExtractWordFontSet(baseStyle == null ? null : baseStyle.RunFonts, themeFonts);
|
||||
}
|
||||
|
||||
private static IDictionary<string, string> BuildThemeFontMap(WordprocessingDocument document)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var themePart = document.MainDocumentPart == null ? null : document.MainDocumentPart.ThemePart;
|
||||
var theme = themePart == null ? null : themePart.Theme;
|
||||
if (theme == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var majorFont = theme.Descendants().FirstOrDefault(item => string.Equals(item.LocalName, "majorFont", StringComparison.OrdinalIgnoreCase));
|
||||
var minorFont = theme.Descendants().FirstOrDefault(item => string.Equals(item.LocalName, "minorFont", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
AddThemeSlot(result, "majorAscii", majorFont, "latin");
|
||||
AddThemeSlot(result, "majorHAnsi", majorFont, "latin");
|
||||
AddThemeSlot(result, "majorEastAsia", majorFont, "ea");
|
||||
AddThemeSlot(result, "majorBidi", majorFont, "cs");
|
||||
AddThemeSlot(result, "minorAscii", minorFont, "latin");
|
||||
AddThemeSlot(result, "minorHAnsi", minorFont, "latin");
|
||||
AddThemeSlot(result, "minorEastAsia", minorFont, "ea");
|
||||
AddThemeSlot(result, "minorBidi", minorFont, "cs");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddThemeSlot(IDictionary<string, string> themeFonts, string token, OpenXmlElement schemeRoot, string localName)
|
||||
{
|
||||
if (schemeRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var element = schemeRoot.Elements().FirstOrDefault(item => string.Equals(item.LocalName, localName, StringComparison.OrdinalIgnoreCase));
|
||||
if (element == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var typeface = GetAttributeValue(element, "typeface");
|
||||
var normalized = NormalizeFontName(typeface);
|
||||
if (!string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
themeFonts[token] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectWordRunFonts(
|
||||
OpenXmlElement root,
|
||||
IDictionary<string, StyleFontSet> styleIndex,
|
||||
FontReferenceSet defaultFonts,
|
||||
IDictionary<string, string> themeFonts,
|
||||
ISet<string> usedFonts)
|
||||
{
|
||||
foreach (var run in root.Descendants<W.Run>())
|
||||
{
|
||||
if (!ContainsVisibleText(run))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var effectiveFonts = ResolveEffectiveWordFonts(run, styleIndex, defaultFonts, themeFonts);
|
||||
foreach (var font in effectiveFonts.GetDefinedFonts())
|
||||
{
|
||||
usedFonts.Add(font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectDrawingFonts(OpenXmlElement root, ISet<string> usedFonts)
|
||||
{
|
||||
foreach (var element in root.Descendants())
|
||||
{
|
||||
if (!(string.Equals(element.LocalName, "latin", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(element.LocalName, "cs", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var typeface = NormalizeFontName(GetAttributeValue(element, "typeface"));
|
||||
if (!string.IsNullOrWhiteSpace(typeface))
|
||||
{
|
||||
usedFonts.Add(typeface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsVisibleText(W.Run run)
|
||||
{
|
||||
if (run == null || string.IsNullOrWhiteSpace(run.InnerText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (run.Ancestors<W.DeletedRun>().Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return run.RunProperties == null || run.RunProperties.Vanish == null;
|
||||
}
|
||||
|
||||
private static FontReferenceSet ResolveEffectiveWordFonts(
|
||||
W.Run run,
|
||||
IDictionary<string, StyleFontSet> styleIndex,
|
||||
FontReferenceSet defaultFonts,
|
||||
IDictionary<string, string> themeFonts)
|
||||
{
|
||||
var effectiveFonts = new FontReferenceSet();
|
||||
effectiveFonts.ApplyOverride(defaultFonts);
|
||||
|
||||
var paragraph = run.Ancestors<W.Paragraph>().FirstOrDefault();
|
||||
if (paragraph != null && paragraph.ParagraphProperties != null)
|
||||
{
|
||||
var paragraphStyleId = paragraph.ParagraphProperties.ParagraphStyleId == null
|
||||
? null
|
||||
: paragraph.ParagraphProperties.ParagraphStyleId.Val == null
|
||||
? null
|
||||
: paragraph.ParagraphProperties.ParagraphStyleId.Val.Value;
|
||||
effectiveFonts.ApplyOverride(ResolveStyleFonts(paragraphStyleId, styleIndex));
|
||||
}
|
||||
|
||||
var table = run.Ancestors<W.Table>().FirstOrDefault();
|
||||
if (table != null && table.TableProperties != null && table.TableProperties.TableStyle != null)
|
||||
{
|
||||
var tableStyleId = table.TableProperties.TableStyle.Val == null ? null : table.TableProperties.TableStyle.Val.Value;
|
||||
effectiveFonts.ApplyOverride(ResolveStyleFonts(tableStyleId, styleIndex));
|
||||
}
|
||||
|
||||
if (run.RunProperties != null && run.RunProperties.RunStyle != null)
|
||||
{
|
||||
var runStyleId = run.RunProperties.RunStyle.Val == null ? null : run.RunProperties.RunStyle.Val.Value;
|
||||
effectiveFonts.ApplyOverride(ResolveStyleFonts(runStyleId, styleIndex));
|
||||
}
|
||||
|
||||
effectiveFonts.ApplyOverride(ExtractWordFontSet(run.RunProperties == null ? null : run.RunProperties.RunFonts, themeFonts));
|
||||
return effectiveFonts;
|
||||
}
|
||||
|
||||
private static FontReferenceSet ResolveStyleFonts(string styleId, IDictionary<string, StyleFontSet> styleIndex)
|
||||
{
|
||||
var result = new FontReferenceSet();
|
||||
if (string.IsNullOrWhiteSpace(styleId) || styleIndex == null || !styleIndex.TryGetValue(styleId, out var style))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var resolved = ResolveStyleFonts(styleId, styleIndex, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
|
||||
result.ApplyOverride(resolved);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static FontReferenceSet ResolveStyleFonts(
|
||||
string styleId,
|
||||
IDictionary<string, StyleFontSet> styleIndex,
|
||||
ISet<string> visited)
|
||||
{
|
||||
var result = new FontReferenceSet();
|
||||
if (string.IsNullOrWhiteSpace(styleId)
|
||||
|| styleIndex == null
|
||||
|| !styleIndex.TryGetValue(styleId, out var style)
|
||||
|| !visited.Add(styleId))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.ApplyOverride(ResolveStyleFonts(style.BasedOnStyleId, styleIndex, visited));
|
||||
result.ApplyOverride(style.Fonts);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static FontReferenceSet ExtractWordFontSet(W.RunFonts runFonts, IDictionary<string, string> themeFonts)
|
||||
{
|
||||
var result = new FontReferenceSet();
|
||||
if (runFonts == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Ascii = ResolveFontName(GetAttributeValue(runFonts, "ascii"), GetAttributeValue(runFonts, "asciiTheme"), themeFonts);
|
||||
result.HighAnsi = ResolveFontName(GetAttributeValue(runFonts, "hAnsi"), GetAttributeValue(runFonts, "hAnsiTheme"), themeFonts);
|
||||
result.EastAsia = ResolveFontName(GetAttributeValue(runFonts, "eastAsia"), GetAttributeValue(runFonts, "eastAsiaTheme"), themeFonts);
|
||||
result.ComplexScript = ResolveFontName(GetAttributeValue(runFonts, "cs"), GetAttributeValue(runFonts, "cstheme"), themeFonts);
|
||||
if (string.IsNullOrWhiteSpace(result.ComplexScript))
|
||||
{
|
||||
result.ComplexScript = ResolveFontName(null, GetAttributeValue(runFonts, "csTheme"), themeFonts);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ResolveFontName(string directValue, string themeToken, IDictionary<string, string> themeFonts)
|
||||
{
|
||||
var normalizedDirect = NormalizeFontName(directValue);
|
||||
if (!string.IsNullOrWhiteSpace(normalizedDirect))
|
||||
{
|
||||
return normalizedDirect;
|
||||
}
|
||||
|
||||
var normalizedToken = NormalizeFontName(themeToken);
|
||||
if (!string.IsNullOrWhiteSpace(normalizedToken)
|
||||
&& themeFonts != null
|
||||
&& themeFonts.TryGetValue(normalizedToken, out var themeFont))
|
||||
{
|
||||
return NormalizeFontName(themeFont);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeFontName(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = value.Trim().Trim('"', '\'');
|
||||
if (normalized.StartsWith("@", StringComparison.Ordinal))
|
||||
{
|
||||
normalized = normalized.Substring(1);
|
||||
}
|
||||
|
||||
return normalized.Length == 0 ? null : normalized;
|
||||
}
|
||||
|
||||
private static string GetAttributeValue(OpenXmlElement element, string localName)
|
||||
{
|
||||
if (element == null || string.IsNullOrWhiteSpace(localName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var attribute in element.GetAttributes())
|
||||
{
|
||||
if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return attribute.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsFontAvailable(string fontName, IDictionary<string, HashSet<string>> documentFontAliases)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fontName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var installedFonts = _installedFonts.Value;
|
||||
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var queue = new Queue<string>();
|
||||
queue.Enqueue(fontName);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = NormalizeFontName(queue.Dequeue());
|
||||
if (string.IsNullOrWhiteSpace(current) || !visited.Add(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (installedFonts.Contains(current))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
EnqueueAliases(current, documentFontAliases, queue);
|
||||
EnqueueAliases(current, KnownFontAliases.Value, queue);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Dictionary<string, HashSet<string>> BuildKnownFontAliases()
|
||||
{
|
||||
var result = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var group in KnownFontAliasGroups)
|
||||
{
|
||||
if (group == null || group.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var index = 0; index < group.Length; index++)
|
||||
{
|
||||
var current = NormalizeFontName(group[index]);
|
||||
if (string.IsNullOrWhiteSpace(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var inner = 0; inner < group.Length; inner++)
|
||||
{
|
||||
if (inner == index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddAliasPair(result, current, group[inner]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void EnqueueAliases(string fontName, IDictionary<string, HashSet<string>> aliases, Queue<string> queue)
|
||||
{
|
||||
if (queue == null || aliases == null || string.IsNullOrWhiteSpace(fontName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!aliases.TryGetValue(fontName, out var values) || values == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
var normalized = NormalizeFontName(value);
|
||||
if (!string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
queue.Enqueue(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddAliasPair(IDictionary<string, HashSet<string>> aliases, string left, string right)
|
||||
{
|
||||
var normalizedLeft = NormalizeFontName(left);
|
||||
var normalizedRight = NormalizeFontName(right);
|
||||
if (string.IsNullOrWhiteSpace(normalizedLeft)
|
||||
|| string.IsNullOrWhiteSpace(normalizedRight)
|
||||
|| string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddAliasValue(aliases, normalizedLeft, normalizedRight);
|
||||
AddAliasValue(aliases, normalizedRight, normalizedLeft);
|
||||
}
|
||||
|
||||
private static void AddAliasValue(IDictionary<string, HashSet<string>> aliases, string key, string value)
|
||||
{
|
||||
if (!aliases.TryGetValue(key, out var values))
|
||||
{
|
||||
values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
aliases[key] = values;
|
||||
}
|
||||
|
||||
values.Add(value);
|
||||
}
|
||||
|
||||
private sealed class StyleFontSet
|
||||
{
|
||||
public string StyleId { get; set; }
|
||||
|
||||
public string BasedOnStyleId { get; set; }
|
||||
|
||||
public FontReferenceSet Fonts { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FontValidationResult
|
||||
{
|
||||
public FontValidationResult(IList<string> usedFonts, IList<string> missingFonts)
|
||||
{
|
||||
UsedFonts = usedFonts ?? Array.Empty<string>();
|
||||
MissingFonts = missingFonts ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public IList<string> UsedFonts { get; private set; }
|
||||
|
||||
public IList<string> MissingFonts { get; private set; }
|
||||
}
|
||||
|
||||
internal sealed class FontReferenceSet
|
||||
{
|
||||
public string Ascii { get; set; }
|
||||
|
||||
public string HighAnsi { get; set; }
|
||||
|
||||
public string EastAsia { get; set; }
|
||||
|
||||
public string ComplexScript { get; set; }
|
||||
|
||||
public IEnumerable<string> GetDefinedFonts()
|
||||
{
|
||||
return new[] { Ascii, HighAnsi, EastAsia, ComplexScript }
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void ApplyOverride(FontReferenceSet other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(other.Ascii))
|
||||
{
|
||||
Ascii = other.Ascii;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(other.HighAnsi))
|
||||
{
|
||||
HighAnsi = other.HighAnsi;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(other.EastAsia))
|
||||
{
|
||||
EastAsia = other.EastAsia;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(other.ComplexScript))
|
||||
{
|
||||
ComplexScript = other.ComplexScript;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
DCIT.Core/Services/IDocConversionService.cs
Normal file
7
DCIT.Core/Services/IDocConversionService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public interface IDocConversionService
|
||||
{
|
||||
void Convert(string sourcePath, string outputPath);
|
||||
}
|
||||
}
|
||||
7
DCIT.Core/Services/IDocumentFieldUpdateService.cs
Normal file
7
DCIT.Core/Services/IDocumentFieldUpdateService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public interface IDocumentFieldUpdateService
|
||||
{
|
||||
void UpdateDynamicContent(string documentPath);
|
||||
}
|
||||
}
|
||||
13
DCIT.Core/Services/IDocumentProcessingService.cs
Normal file
13
DCIT.Core/Services/IDocumentProcessingService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using DCIT.Core.Models;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public interface IDocumentProcessingService
|
||||
{
|
||||
FileProcessResult Replace(ReplaceFileRequest request, IProgress<FileProcessProgress> progress, CancellationToken cancellationToken);
|
||||
|
||||
FileProcessResult Restore(RestoreFileRequest request, IProgress<FileProcessProgress> progress, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
9
DCIT.Core/Services/IFontValidationService.cs
Normal file
9
DCIT.Core/Services/IFontValidationService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public interface IFontValidationService
|
||||
{
|
||||
FontValidationResult Validate(string docxPath);
|
||||
|
||||
FontValidationResult EnsureFontsAvailable(string docxPath);
|
||||
}
|
||||
}
|
||||
110
DCIT.Core/Services/LogService.cs
Normal file
110
DCIT.Core/Services/LogService.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using DCIT.Core.Models;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class LogService
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly string _logFilePath;
|
||||
private bool _runtimeFailureActive;
|
||||
|
||||
public LogService(string startupDirectory)
|
||||
{
|
||||
_logFilePath = Path.Combine(startupDirectory, string.Format("session-{0:yyyyMMdd}.log", DateTime.Today));
|
||||
EnsureWritable();
|
||||
}
|
||||
|
||||
public event EventHandler<LogEntry> EntryWritten;
|
||||
|
||||
public event EventHandler<LogWriteFailureEventArgs> WriteFailed;
|
||||
|
||||
public string LogFilePath
|
||||
{
|
||||
get { return _logFilePath; }
|
||||
}
|
||||
|
||||
public bool HasRuntimeFailure { get; private set; }
|
||||
|
||||
public Exception LastRuntimeFailure { get; private set; }
|
||||
|
||||
public void Info(string eventCode, string message, string detail = null)
|
||||
{
|
||||
Write(LogLevel.Info, eventCode, message, detail);
|
||||
}
|
||||
|
||||
public void Warning(string eventCode, string message, string detail = null)
|
||||
{
|
||||
Write(LogLevel.Warning, eventCode, message, detail);
|
||||
}
|
||||
|
||||
public void Error(string eventCode, string message, string detail = null)
|
||||
{
|
||||
Write(LogLevel.Error, eventCode, message, detail);
|
||||
}
|
||||
|
||||
public void Write(LogLevel level, string eventCode, string message, string detail = null)
|
||||
{
|
||||
var entry = new LogEntry
|
||||
{
|
||||
Level = level,
|
||||
EventCode = eventCode,
|
||||
Message = message,
|
||||
Detail = detail,
|
||||
};
|
||||
|
||||
var line = string.Format(
|
||||
"{0:yyyy-MM-dd HH:mm:ss.fff}\t{1}\t{2}\t{3}",
|
||||
entry.Timestamp,
|
||||
level.ToString().ToUpperInvariant(),
|
||||
entry.EventCode ?? string.Empty,
|
||||
entry.Message ?? string.Empty);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entry.Detail))
|
||||
{
|
||||
line += "\t" + entry.Detail.Replace("\r", " ").Replace("\n", " ");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
File.AppendAllText(_logFilePath, line + Environment.NewLine, new UTF8Encoding(false));
|
||||
_runtimeFailureActive = false;
|
||||
HasRuntimeFailure = false;
|
||||
LastRuntimeFailure = null;
|
||||
}
|
||||
|
||||
EntryWritten?.Invoke(this, entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var shouldRaiseEvent = false;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
HasRuntimeFailure = true;
|
||||
LastRuntimeFailure = ex;
|
||||
if (!_runtimeFailureActive)
|
||||
{
|
||||
_runtimeFailureActive = true;
|
||||
shouldRaiseEvent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRaiseEvent)
|
||||
{
|
||||
WriteFailed?.Invoke(this, new LogWriteFailureEventArgs(entry, _logFilePath, ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureWritable()
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_logFilePath);
|
||||
Directory.CreateDirectory(directory);
|
||||
File.AppendAllText(_logFilePath, string.Empty, new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
518
DCIT.Core/Services/OfficeDocConversionService.cs
Normal file
518
DCIT.Core/Services/OfficeDocConversionService.cs
Normal file
@@ -0,0 +1,518 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class OfficeDocConversionService : IDocConversionService, IDocumentFieldUpdateService
|
||||
{
|
||||
private const int WdFormatDocument97 = 0;
|
||||
private const int WdFormatDocumentDefault = 16;
|
||||
private static readonly string[] WpsProgIds =
|
||||
{
|
||||
"KWPS.Application",
|
||||
"kwps.Application",
|
||||
"WPS.Application",
|
||||
"wps.Application",
|
||||
};
|
||||
|
||||
private readonly string _applicationBaseDirectory;
|
||||
|
||||
public OfficeDocConversionService(string applicationBaseDirectory = null)
|
||||
{
|
||||
_applicationBaseDirectory = string.IsNullOrWhiteSpace(applicationBaseDirectory)
|
||||
? AppContext.BaseDirectory
|
||||
: applicationBaseDirectory;
|
||||
}
|
||||
|
||||
public void Convert(string sourcePath, string outputPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sourcePath))
|
||||
{
|
||||
throw new ArgumentNullException("sourcePath");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
throw new ArgumentNullException("outputPath");
|
||||
}
|
||||
|
||||
var sourceExtension = NormalizeExtension(sourcePath);
|
||||
var outputExtension = NormalizeExtension(outputPath);
|
||||
if (!IsSupportedConversion(sourceExtension, outputExtension))
|
||||
{
|
||||
throw new NotSupportedException("仅支持 .doc 与 .docx 之间互相转换。");
|
||||
}
|
||||
|
||||
var outputDirectory = Path.GetDirectoryName(outputPath);
|
||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
File.Delete(outputPath);
|
||||
}
|
||||
|
||||
var errors = new List<string>();
|
||||
foreach (var progId in GetOfficeAutomationProgIds())
|
||||
{
|
||||
try
|
||||
{
|
||||
ConvertWithOfficeAutomationBackend(progId, sourcePath, outputPath, outputExtension);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(progId + ": " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
var docToPath = ResolveDocToPath();
|
||||
if (!string.IsNullOrWhiteSpace(docToPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
ConvertWithDocToBackend(docToPath, sourcePath, outputPath, outputExtension);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add("DocTo: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"未找到可用的 .doc 转换链路。请确认已安装可自动化的 Microsoft Word 或 WPS;如自动化不可用,可提供 docto.exe 作为兜底转换工具。"
|
||||
+ Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, errors.Where(item => !string.IsNullOrWhiteSpace(item))));
|
||||
}
|
||||
|
||||
public void UpdateDynamicContent(string documentPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(documentPath))
|
||||
{
|
||||
throw new ArgumentNullException("documentPath");
|
||||
}
|
||||
|
||||
if (!File.Exists(documentPath))
|
||||
{
|
||||
throw new FileNotFoundException("要更新动态内容的文档不存在。", documentPath);
|
||||
}
|
||||
|
||||
var extension = NormalizeExtension(documentPath);
|
||||
if (!string.Equals(extension, ".docx", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(extension, ".doc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new NotSupportedException("动态内容更新仅支持 .doc 或 .docx 文档。");
|
||||
}
|
||||
|
||||
var errors = new List<string>();
|
||||
foreach (var progId in GetOfficeAutomationProgIds())
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateDynamicContentWithOfficeAutomationBackend(progId, documentPath);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(progId + ": " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"未找到可用的目录/域刷新链路。请确保已安装可自动化的 Microsoft Word 或 WPS。"
|
||||
+ Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, errors.Where(item => !string.IsNullOrWhiteSpace(item))));
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<string> GetOfficeAutomationProgIds()
|
||||
{
|
||||
yield return "Word.Application";
|
||||
foreach (var progId in WpsProgIds)
|
||||
{
|
||||
yield return progId;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual string ResolveDocToPath()
|
||||
{
|
||||
var candidates = new List<string>();
|
||||
var envPath = Environment.GetEnvironmentVariable("DCIT_DOCTO_PATH");
|
||||
if (!string.IsNullOrWhiteSpace(envPath))
|
||||
{
|
||||
candidates.Add(envPath);
|
||||
}
|
||||
|
||||
candidates.Add(Path.Combine(_applicationBaseDirectory, "docto.exe"));
|
||||
candidates.Add(Path.Combine(_applicationBaseDirectory, "tools", "DocTo", "docto.exe"));
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
foreach (var segment in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
try
|
||||
{
|
||||
var candidate = Path.Combine(segment.Trim(), "docto.exe");
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual void ConvertWithDocToBackend(string docToPath, string sourcePath, string outputPath, string outputExtension)
|
||||
{
|
||||
var format = string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase)
|
||||
? "wdFormatDocumentDefault"
|
||||
: "wdFormatDocument97";
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = docToPath,
|
||||
Arguments = string.Format("-F \"{0}\" -O \"{1}\" -T {2}", sourcePath, outputPath, format),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = Path.GetDirectoryName(docToPath) ?? _applicationBaseDirectory,
|
||||
};
|
||||
|
||||
using (var process = Process.Start(startInfo))
|
||||
{
|
||||
if (process == null)
|
||||
{
|
||||
throw new InvalidOperationException("无法启动 docto.exe。");
|
||||
}
|
||||
|
||||
var standardOutput = process.StandardOutput.ReadToEnd();
|
||||
var standardError = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit(120000);
|
||||
|
||||
if (process.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"docto.exe 转换失败。"
|
||||
+ Environment.NewLine
|
||||
+ (string.IsNullOrWhiteSpace(standardOutput) ? string.Empty : standardOutput + Environment.NewLine)
|
||||
+ standardError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ConvertWithOfficeAutomationBackend(string progId, string sourcePath, string outputPath, string outputExtension)
|
||||
{
|
||||
ConvertWithWordAutomation(progId, sourcePath, outputPath, outputExtension);
|
||||
}
|
||||
|
||||
protected virtual void UpdateDynamicContentWithOfficeAutomationBackend(string progId, string documentPath)
|
||||
{
|
||||
UpdateDynamicContentWithWordAutomation(progId, documentPath);
|
||||
}
|
||||
|
||||
private static bool IsSupportedConversion(string sourceExtension, string outputExtension)
|
||||
{
|
||||
if (string.Equals(sourceExtension, outputExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string.Equals(sourceExtension, ".doc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(sourceExtension, ".docx", StringComparison.OrdinalIgnoreCase))
|
||||
&& (string.Equals(outputExtension, ".doc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static string NormalizeExtension(string path)
|
||||
{
|
||||
return Path.GetExtension(path) ?? string.Empty;
|
||||
}
|
||||
|
||||
private static void ConvertWithWordAutomation(string progId, string sourcePath, string outputPath, string outputExtension)
|
||||
{
|
||||
var appType = Type.GetTypeFromProgID(progId, false);
|
||||
if (appType == null)
|
||||
{
|
||||
throw new InvalidOperationException("未注册 COM ProgID: " + progId);
|
||||
}
|
||||
|
||||
object application = null;
|
||||
object documents = null;
|
||||
object document = null;
|
||||
|
||||
try
|
||||
{
|
||||
application = Activator.CreateInstance(appType);
|
||||
if (application == null)
|
||||
{
|
||||
throw new InvalidOperationException("无法创建 COM 实例: " + progId);
|
||||
}
|
||||
|
||||
TrySetProperty(application, "Visible", false);
|
||||
TrySetProperty(application, "DisplayAlerts", 0);
|
||||
documents = GetProperty(application, "Documents");
|
||||
if (documents == null)
|
||||
{
|
||||
throw new InvalidOperationException("无法获取 Documents 集合。");
|
||||
}
|
||||
|
||||
document = InvokeMethod(documents, "Open", sourcePath, false, true);
|
||||
if (document == null)
|
||||
{
|
||||
throw new InvalidOperationException("无法打开文档。");
|
||||
}
|
||||
|
||||
var fileFormat = string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase)
|
||||
? WdFormatDocumentDefault
|
||||
: WdFormatDocument97;
|
||||
|
||||
try
|
||||
{
|
||||
InvokeMethod(document, "SaveAs2", outputPath, fileFormat);
|
||||
}
|
||||
catch (MissingMethodException)
|
||||
{
|
||||
InvokeMethod(document, "SaveAs", outputPath, fileFormat);
|
||||
}
|
||||
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
throw new InvalidOperationException("Office 自动化未生成目标文件。");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
SafeCloseDocument(document);
|
||||
SafeQuitApplication(application);
|
||||
ReleaseComObject(document);
|
||||
ReleaseComObject(documents);
|
||||
ReleaseComObject(application);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateDynamicContentWithWordAutomation(string progId, string documentPath)
|
||||
{
|
||||
var appType = Type.GetTypeFromProgID(progId, false);
|
||||
if (appType == null)
|
||||
{
|
||||
throw new InvalidOperationException("未注册 COM ProgID: " + progId);
|
||||
}
|
||||
|
||||
object application = null;
|
||||
object documents = null;
|
||||
object document = null;
|
||||
|
||||
try
|
||||
{
|
||||
application = Activator.CreateInstance(appType);
|
||||
if (application == null)
|
||||
{
|
||||
throw new InvalidOperationException("无法创建 COM 实例: " + progId);
|
||||
}
|
||||
|
||||
TrySetProperty(application, "Visible", false);
|
||||
TrySetProperty(application, "DisplayAlerts", 0);
|
||||
documents = GetProperty(application, "Documents");
|
||||
if (documents == null)
|
||||
{
|
||||
throw new InvalidOperationException("无法获取 Documents 集合。");
|
||||
}
|
||||
|
||||
document = InvokeMethod(documents, "Open", documentPath, false, false);
|
||||
if (document == null)
|
||||
{
|
||||
throw new InvalidOperationException("无法打开文档。");
|
||||
}
|
||||
|
||||
TryInvokeMethod(document, "Activate");
|
||||
TryInvokeMethod(document, "Repaginate");
|
||||
TryUpdateCollectionItems(document, "TablesOfContents", "Update", "UpdatePageNumbers");
|
||||
TryUpdateCollectionItems(document, "TablesOfFigures", "Update");
|
||||
TryUpdateCollectionItems(document, "TablesOfAuthorities", "Update");
|
||||
TryUpdateCollection(document, "Fields", "Update");
|
||||
TryUpdateCollection(document, "Indexes", "Update");
|
||||
TryInvokeMethod(document, "Repaginate");
|
||||
InvokeMethod(document, "Save");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SafeCloseDocument(document);
|
||||
SafeQuitApplication(application);
|
||||
ReleaseComObject(document);
|
||||
ReleaseComObject(documents);
|
||||
ReleaseComObject(application);
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetProperty(object instance, string propertyName)
|
||||
{
|
||||
return instance.GetType().InvokeMember(
|
||||
propertyName,
|
||||
System.Reflection.BindingFlags.GetProperty,
|
||||
null,
|
||||
instance,
|
||||
null);
|
||||
}
|
||||
|
||||
private static void TrySetProperty(object instance, string propertyName, object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
instance.GetType().InvokeMember(
|
||||
propertyName,
|
||||
System.Reflection.BindingFlags.SetProperty,
|
||||
null,
|
||||
instance,
|
||||
new[] { value });
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static object InvokeMethod(object instance, string methodName, params object[] arguments)
|
||||
{
|
||||
return instance.GetType().InvokeMember(
|
||||
methodName,
|
||||
System.Reflection.BindingFlags.InvokeMethod,
|
||||
null,
|
||||
instance,
|
||||
arguments);
|
||||
}
|
||||
|
||||
private static bool TryInvokeMethod(object instance, string methodName, params object[] arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
InvokeMethod(instance, methodName, arguments);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryUpdateCollection(object instance, string propertyName, string updateMethodName)
|
||||
{
|
||||
object collection = null;
|
||||
try
|
||||
{
|
||||
collection = GetProperty(instance, propertyName);
|
||||
if (collection == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryInvokeMethod(collection, updateMethodName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseComObject(collection);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryUpdateCollectionItems(object instance, string propertyName, params string[] methodNames)
|
||||
{
|
||||
object collection = null;
|
||||
try
|
||||
{
|
||||
collection = GetProperty(instance, propertyName);
|
||||
if (collection == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var countObject = GetProperty(collection, "Count");
|
||||
var count = System.Convert.ToInt32(countObject);
|
||||
for (var index = 1; index <= count; index++)
|
||||
{
|
||||
object item = null;
|
||||
try
|
||||
{
|
||||
item = InvokeMethod(collection, "Item", index);
|
||||
foreach (var methodName in methodNames.Where(name => !string.IsNullOrWhiteSpace(name)))
|
||||
{
|
||||
TryInvokeMethod(item, methodName);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseComObject(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseComObject(collection);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SafeCloseDocument(object document)
|
||||
{
|
||||
if (document == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InvokeMethod(document, "Close", false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void SafeQuitApplication(object application)
|
||||
{
|
||||
if (application == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InvokeMethod(application, "Quit", false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReleaseComObject(object instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (instance != null && Marshal.IsComObject(instance))
|
||||
{
|
||||
Marshal.FinalReleaseComObject(instance);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
376
DCIT.Core/Services/RuleService.cs
Normal file
376
DCIT.Core/Services/RuleService.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using DCIT.Core.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class RuleService
|
||||
{
|
||||
private static readonly TimeSpan RegexValidationTimeout = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
public RuleFile Load(string ruleFilePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ruleFilePath) || !System.IO.File.Exists(ruleFilePath))
|
||||
{
|
||||
return new RuleFile();
|
||||
}
|
||||
|
||||
var content = System.IO.File.ReadAllText(ruleFilePath, Encoding.UTF8);
|
||||
return JsonConvert.DeserializeObject<RuleFile>(content) ?? new RuleFile();
|
||||
}
|
||||
|
||||
public void Save(string ruleFilePath, RuleFile ruleFile)
|
||||
{
|
||||
var report = Validate(ruleFile);
|
||||
if (report.HasErrors)
|
||||
{
|
||||
var detail = string.Join(Environment.NewLine, report.Issues.Select(FormatIssue));
|
||||
throw new InvalidOperationException(detail);
|
||||
}
|
||||
|
||||
var content = JsonConvert.SerializeObject(ruleFile, Formatting.Indented);
|
||||
System.IO.File.WriteAllText(ruleFilePath, content, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
public RuleValidationReport Validate(RuleFile ruleFile)
|
||||
{
|
||||
ruleFile = ruleFile ?? new RuleFile();
|
||||
|
||||
var report = new RuleValidationReport();
|
||||
var idSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var validatedRules = new List<ValidatedRuleContext>();
|
||||
|
||||
for (var index = 0; index < ruleFile.Rules.Count; index++)
|
||||
{
|
||||
var rule = ruleFile.Rules[index] ?? new RuleDefinition();
|
||||
if (string.IsNullOrWhiteSpace(rule.Id))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "规则 ID 不能为空。"));
|
||||
}
|
||||
else if (!idSet.Add(rule.Id.Trim()))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "规则 ID 重复。"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rule.Name))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "规则名称为空。"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rule.SearchText))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "原文本或表达式不能为空。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
Regex compiledRegex = null;
|
||||
if (rule.MatchMode == RuleMatchMode.RegularExpression)
|
||||
{
|
||||
try
|
||||
{
|
||||
compiledRegex = new Regex(rule.SearchText, GetRegexOptions(rule), RegexValidationTimeout);
|
||||
ValidateRegexRule(report, index, rule, compiledRegex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "正则表达式非法: " + ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.MatchMode == RuleMatchMode.PlainText
|
||||
&& !string.IsNullOrEmpty(rule.ReplacementText)
|
||||
&& rule.ReplacementText.IndexOf(rule.SearchText, rule.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "替换文本会再次命中原文本,请确认是否符合预期。"));
|
||||
}
|
||||
|
||||
if (rule.MatchMode == RuleMatchMode.RegularExpression
|
||||
&& compiledRegex != null
|
||||
&& !string.IsNullOrEmpty(rule.ReplacementText)
|
||||
&& IsRegexReplacementSelfMatching(compiledRegex, rule.ReplacementText))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "替换模板会再次命中当前规则,请确认是否符合预期。"));
|
||||
}
|
||||
|
||||
var current = new ValidatedRuleContext(index, rule, compiledRegex);
|
||||
foreach (var previous in validatedRules)
|
||||
{
|
||||
AddCompatibilityIssues(report, previous, current);
|
||||
}
|
||||
|
||||
validatedRules.Add(current);
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
public RuleDefinition CreateAutoExtractRule(ExtractionCandidate candidate, IList<RuleDefinition> existingRules)
|
||||
{
|
||||
var prefix = GetPrefix(candidate.PrimaryCategory);
|
||||
var namePrefix = GetNamePrefix(candidate.PrimaryCategory);
|
||||
var nextIndex = 1;
|
||||
var existingIds = new HashSet<string>(existingRules.Select(rule => rule.Id ?? string.Empty), StringComparer.OrdinalIgnoreCase);
|
||||
var existingNames = new HashSet<string>(existingRules.Select(rule => rule.Name ?? string.Empty), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
string id;
|
||||
do
|
||||
{
|
||||
id = string.Format("{0}{1:0000}", prefix, nextIndex++);
|
||||
}
|
||||
while (existingIds.Contains(id));
|
||||
|
||||
nextIndex = 1;
|
||||
string name;
|
||||
do
|
||||
{
|
||||
name = string.Format("{0}-{1:0000}", namePrefix, nextIndex++);
|
||||
}
|
||||
while (existingNames.Contains(name));
|
||||
|
||||
return new RuleDefinition
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
IsEnabled = true,
|
||||
MatchMode = RuleMatchMode.PlainText,
|
||||
IsCaseSensitive = false,
|
||||
IsWholeWord = false,
|
||||
SearchText = candidate.OriginalText,
|
||||
ReplacementText = string.Empty,
|
||||
Note = string.Format(
|
||||
"自动提取; 类型={0}; 统计={1}; 首次文档={2}; 时间={3:yyyy-MM-dd HH:mm:ss}",
|
||||
candidate.CategoryDisplay,
|
||||
candidate.StatisticDisplay,
|
||||
candidate.FirstDocumentPath,
|
||||
DateTime.Now),
|
||||
};
|
||||
}
|
||||
|
||||
public string FormatIssue(RuleValidationIssue issue)
|
||||
{
|
||||
return string.Format(
|
||||
"[{0}] 规则#{1} ({2}/{3}) {4}",
|
||||
issue.Severity,
|
||||
issue.Index + 1,
|
||||
issue.RuleId ?? "-",
|
||||
issue.RuleName ?? "-",
|
||||
issue.Message);
|
||||
}
|
||||
|
||||
private static RuleValidationIssue CreateIssue(RuleValidationSeverity severity, int index, RuleDefinition rule, string message)
|
||||
{
|
||||
return new RuleValidationIssue
|
||||
{
|
||||
Severity = severity,
|
||||
Index = index,
|
||||
RuleId = rule.Id,
|
||||
RuleName = rule.Name,
|
||||
Message = message,
|
||||
};
|
||||
}
|
||||
|
||||
private static RegexOptions GetRegexOptions(RuleDefinition rule)
|
||||
{
|
||||
var options = RegexOptions.CultureInvariant;
|
||||
if (!rule.IsCaseSensitive)
|
||||
{
|
||||
options |= RegexOptions.IgnoreCase;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void ValidateRegexRule(RuleValidationReport report, int index, RuleDefinition rule, Regex regex)
|
||||
{
|
||||
if (MatchesEmptyText(regex))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "正则表达式可匹配零长度文本,执行时可能导致死循环。"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
regex.Replace("validation", rule.ReplacementText ?? string.Empty, 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "替换模板非法: " + ex.Message));
|
||||
}
|
||||
|
||||
if (HasNestedQuantifierRisk(rule.SearchText))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "正则表达式存在嵌套量词风险,可能导致性能急剧下降。"));
|
||||
}
|
||||
|
||||
if (MayTimeoutDuringValidation(regex))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "正则表达式验证探测时出现超时风险,建议简化表达式。"));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool MatchesEmptyText(Regex regex)
|
||||
{
|
||||
var emptyMatch = regex.Match(string.Empty);
|
||||
if (emptyMatch.Success && emptyMatch.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var sampleMatch = regex.Match("A");
|
||||
return sampleMatch.Success && sampleMatch.Length == 0;
|
||||
}
|
||||
|
||||
private static bool IsRegexReplacementSelfMatching(Regex regex, string replacementText)
|
||||
{
|
||||
try
|
||||
{
|
||||
return regex.Match(replacementText ?? string.Empty).Success;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool MayTimeoutDuringValidation(Regex regex)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = regex.IsMatch(new string('a', 4096) + "!");
|
||||
return false;
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasNestedQuantifierRisk(string pattern)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Regex.IsMatch(
|
||||
pattern,
|
||||
@"\((?:[^()\\]|\\.)+[+*](?:[^()\\]|\\.)*\)[+*{]",
|
||||
RegexOptions.CultureInvariant);
|
||||
}
|
||||
|
||||
private static void AddCompatibilityIssues(RuleValidationReport report, ValidatedRuleContext previous, ValidatedRuleContext current)
|
||||
{
|
||||
if (previous.Rule.Target != current.Rule.Target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsEquivalentSearchText(previous.Rule.SearchText, current.Rule.SearchText))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(
|
||||
RuleValidationSeverity.Warning,
|
||||
current.Index,
|
||||
current.Rule,
|
||||
string.Format("当前规则与 #{0} 使用相同原文本或表达式,请确认顺序影响。", previous.Index + 1)));
|
||||
|
||||
if (HaveDifferentMatchingOptions(previous.Rule, current.Rule))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(
|
||||
RuleValidationSeverity.Warning,
|
||||
current.Index,
|
||||
current.Rule,
|
||||
string.Format("当前规则与 #{0} 使用相同原文本但匹配选项或替换文本不同,继续执行结果可能不稳定。", previous.Index + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
if (previous.Rule.MatchMode == RuleMatchMode.PlainText
|
||||
&& current.Rule.MatchMode == RuleMatchMode.PlainText
|
||||
&& HasPlainTextOverlap(previous.Rule, current.Rule))
|
||||
{
|
||||
report.Issues.Add(CreateIssue(
|
||||
RuleValidationSeverity.Warning,
|
||||
current.Index,
|
||||
current.Rule,
|
||||
string.Format("当前规则与 #{0} 存在重叠命中风险,请确认执行顺序。", previous.Index + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsEquivalentSearchText(string left, string right)
|
||||
{
|
||||
return string.Equals(left ?? string.Empty, right ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool HaveDifferentMatchingOptions(RuleDefinition left, RuleDefinition right)
|
||||
{
|
||||
return left.MatchMode != right.MatchMode
|
||||
|| left.IsCaseSensitive != right.IsCaseSensitive
|
||||
|| left.IsWholeWord != right.IsWholeWord
|
||||
|| !string.Equals(left.ReplacementText ?? string.Empty, right.ReplacementText ?? string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool HasPlainTextOverlap(RuleDefinition left, RuleDefinition right)
|
||||
{
|
||||
if (string.IsNullOrEmpty(left.SearchText) || string.IsNullOrEmpty(right.SearchText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left.IsWholeWord && right.IsWholeWord)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var comparison = left.IsCaseSensitive && right.IsCaseSensitive
|
||||
? StringComparison.Ordinal
|
||||
: StringComparison.OrdinalIgnoreCase;
|
||||
|
||||
return left.SearchText.IndexOf(right.SearchText, comparison) >= 0
|
||||
|| right.SearchText.IndexOf(left.SearchText, comparison) >= 0;
|
||||
}
|
||||
|
||||
private static string GetPrefix(AutoExtractCategory category)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case AutoExtractCategory.Keyword:
|
||||
return "AK";
|
||||
case AutoExtractCategory.HighFrequencyWord:
|
||||
return "AW";
|
||||
default:
|
||||
return "AS";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetNamePrefix(AutoExtractCategory category)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case AutoExtractCategory.Keyword:
|
||||
return "AUTO-KW";
|
||||
case AutoExtractCategory.HighFrequencyWord:
|
||||
return "AUTO-WORD";
|
||||
default:
|
||||
return "AUTO-SENT";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ValidatedRuleContext
|
||||
{
|
||||
public ValidatedRuleContext(int index, RuleDefinition rule, Regex compiledRegex)
|
||||
{
|
||||
Index = index;
|
||||
Rule = rule;
|
||||
CompiledRegex = compiledRegex;
|
||||
}
|
||||
|
||||
public int Index { get; private set; }
|
||||
|
||||
public RuleDefinition Rule { get; private set; }
|
||||
|
||||
public Regex CompiledRegex { get; private set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
143
DCIT.Core/Services/RuntimeEnvironmentValidationService.cs
Normal file
143
DCIT.Core/Services/RuntimeEnvironmentValidationService.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using DCIT.Core.Models;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class RuntimeEnvironmentValidationService
|
||||
{
|
||||
private static readonly string[] WpsProgIds =
|
||||
{
|
||||
"KWPS.Application",
|
||||
"kwps.Application",
|
||||
"WPS.Application",
|
||||
"wps.Application",
|
||||
};
|
||||
|
||||
private readonly string _startupDirectory;
|
||||
|
||||
public RuntimeEnvironmentValidationService(string startupDirectory = null)
|
||||
{
|
||||
_startupDirectory = string.IsNullOrWhiteSpace(startupDirectory)
|
||||
? AppContext.BaseDirectory
|
||||
: startupDirectory;
|
||||
}
|
||||
|
||||
public RuntimeEnvironmentValidationResult Validate()
|
||||
{
|
||||
var result = new RuntimeEnvironmentValidationResult
|
||||
{
|
||||
StartupDirectory = _startupDirectory,
|
||||
};
|
||||
|
||||
result.IsStartupDirectoryWritable = CanWriteToDirectory(_startupDirectory);
|
||||
if (!result.IsStartupDirectoryWritable)
|
||||
{
|
||||
result.BlockingIssues.Add("程序启动目录不可写。请将程序部署到当前用户具有写权限的目录后再启动。");
|
||||
}
|
||||
|
||||
result.ResolvedAutomationProgId = ResolveAutomationProgId();
|
||||
if (string.IsNullOrWhiteSpace(result.ResolvedAutomationProgId))
|
||||
{
|
||||
result.BlockingIssues.Add("未检测到可自动化的 Microsoft Word 或 WPS 组件。当前环境无法满足目录/域刷新及 .doc 处理要求。");
|
||||
}
|
||||
|
||||
result.ResolvedDocToPath = ResolveDocToPath();
|
||||
if (string.IsNullOrWhiteSpace(result.ResolvedDocToPath))
|
||||
{
|
||||
result.Warnings.Add("未检测到 docto.exe,.doc 兼容性将仅依赖 Word/WPS 自动化链路。");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<string> GetOfficeAutomationProgIds()
|
||||
{
|
||||
yield return "Word.Application";
|
||||
foreach (var progId in WpsProgIds)
|
||||
{
|
||||
yield return progId;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool IsProgIdRegistered(string progId)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(progId)
|
||||
&& Type.GetTypeFromProgID(progId, false) != null;
|
||||
}
|
||||
|
||||
protected virtual string ResolveDocToPath()
|
||||
{
|
||||
var candidates = new List<string>();
|
||||
var envPath = Environment.GetEnvironmentVariable("DCIT_DOCTO_PATH");
|
||||
if (!string.IsNullOrWhiteSpace(envPath))
|
||||
{
|
||||
candidates.Add(envPath);
|
||||
}
|
||||
|
||||
candidates.Add(Path.Combine(_startupDirectory, "docto.exe"));
|
||||
candidates.Add(Path.Combine(_startupDirectory, "tools", "DocTo", "docto.exe"));
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
foreach (var segment in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
try
|
||||
{
|
||||
var candidate = Path.Combine(segment.Trim(), "docto.exe");
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual bool CanWriteToDirectory(string directoryPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
var probePath = Path.Combine(directoryPath, ".dcit-writecheck-" + Guid.NewGuid().ToString("N") + ".tmp");
|
||||
File.WriteAllText(probePath, "probe");
|
||||
File.Delete(probePath);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveAutomationProgId()
|
||||
{
|
||||
foreach (var progId in GetOfficeAutomationProgIds())
|
||||
{
|
||||
if (IsProgIdRegistered(progId))
|
||||
{
|
||||
return progId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
183
DCIT.Core/Services/TemporaryFileCleanupService.cs
Normal file
183
DCIT.Core/Services/TemporaryFileCleanupService.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using DCIT.Core.Models;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public sealed class TemporaryFileCleanupService
|
||||
{
|
||||
private static readonly Regex AtomicTempFilePattern = new Regex(@"\.tmp-[0-9a-fA-F]{32}(?=\.)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private readonly string _tempRoot;
|
||||
|
||||
public TemporaryFileCleanupService()
|
||||
: this(Path.Combine(Path.GetTempPath(), "DCIT"))
|
||||
{
|
||||
}
|
||||
|
||||
public TemporaryFileCleanupService(string tempRoot)
|
||||
{
|
||||
_tempRoot = string.IsNullOrWhiteSpace(tempRoot)
|
||||
? Path.Combine(Path.GetTempPath(), "DCIT")
|
||||
: tempRoot;
|
||||
}
|
||||
|
||||
public TemporaryCleanupResult Cleanup(AppConfig config)
|
||||
{
|
||||
var result = new TemporaryCleanupResult();
|
||||
CleanupDedicatedTempDirectory(Path.Combine(_tempRoot, "processing"), result);
|
||||
CleanupDedicatedTempDirectory(Path.Combine(_tempRoot, "extract"), result);
|
||||
|
||||
var configuredOutputDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
AddDirectory(configuredOutputDirectories, config == null || config.ReplacePage == null ? null : config.ReplacePage.OutputDirectory);
|
||||
AddDirectory(configuredOutputDirectories, config == null || config.RestorePage == null ? null : config.RestorePage.RestoreDirectory);
|
||||
|
||||
foreach (var directory in configuredOutputDirectories)
|
||||
{
|
||||
CleanupAtomicTempFiles(directory, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddDirectory(ISet<string> directories, string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
directories.Add(Path.GetFullPath(path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void CleanupAtomicTempFiles(string rootDirectory, TemporaryCleanupResult result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<string> files;
|
||||
try
|
||||
{
|
||||
files = Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories)
|
||||
.Where(path => IsAtomicTempFile(Path.GetFileName(path)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Failures.Add("无法扫描目录 " + rootDirectory + ": " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
TryDeleteFile(file, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAtomicTempFile(string fileName)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(fileName) && AtomicTempFilePattern.IsMatch(fileName);
|
||||
}
|
||||
|
||||
private static void CleanupDedicatedTempDirectory(string rootDirectory, TemporaryCleanupResult result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<string> files;
|
||||
try
|
||||
{
|
||||
files = Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Failures.Add("无法扫描目录 " + rootDirectory + ": " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
TryDeleteFile(file, result);
|
||||
}
|
||||
|
||||
TryDeleteEmptyDirectories(rootDirectory, result);
|
||||
}
|
||||
|
||||
private static void TryDeleteEmptyDirectories(string rootDirectory, TemporaryCleanupResult result)
|
||||
{
|
||||
IEnumerable<string> directories;
|
||||
try
|
||||
{
|
||||
directories = Directory.EnumerateDirectories(rootDirectory, "*", SearchOption.AllDirectories)
|
||||
.OrderByDescending(path => path.Length)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Failures.Add("无法枚举临时目录 " + rootDirectory + ": " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.EnumerateFileSystemEntries(directory).Any())
|
||||
{
|
||||
Directory.Delete(directory);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Failures.Add("无法删除临时目录 " + directory + ": " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.EnumerateFileSystemEntries(rootDirectory).Any())
|
||||
{
|
||||
Directory.Delete(rootDirectory);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Failures.Add("无法删除临时目录 " + rootDirectory + ": " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string filePath, TemporaryCleanupResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
result.RemovedFiles.Add(filePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Failures.Add("无法删除临时文件 " + filePath + ": " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TemporaryCleanupResult
|
||||
{
|
||||
public IList<string> RemovedFiles { get; } = new List<string>();
|
||||
|
||||
public IList<string> Failures { get; } = new List<string>();
|
||||
}
|
||||
}
|
||||
212
DCIT.Core/Services/WordTextExtractionService.cs
Normal file
212
DCIT.Core/Services/WordTextExtractionService.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DCIT.Core.Models;
|
||||
using DCIT.Core.Utilities;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using W = DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace DCIT.Core.Services
|
||||
{
|
||||
public class WordTextExtractionService
|
||||
{
|
||||
private readonly IDocConversionService _docConversionService;
|
||||
|
||||
public WordTextExtractionService()
|
||||
: this(new OfficeDocConversionService())
|
||||
{
|
||||
}
|
||||
|
||||
public WordTextExtractionService(IDocConversionService docConversionService)
|
||||
{
|
||||
_docConversionService = docConversionService;
|
||||
}
|
||||
|
||||
public ExtractedDocumentText ExtractVisibleText(string filePath)
|
||||
{
|
||||
var workingPath = filePath;
|
||||
string temporaryDocxPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(filePath) ?? string.Empty;
|
||||
if (extension.Equals(".doc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_docConversionService == null)
|
||||
{
|
||||
throw new NotSupportedException("当前环境未配置 .doc 转换服务。");
|
||||
}
|
||||
|
||||
temporaryDocxPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"DCIT",
|
||||
"extract",
|
||||
Guid.NewGuid().ToString("N") + ".docx");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(temporaryDocxPath) ?? Path.GetTempPath());
|
||||
_docConversionService.Convert(filePath, temporaryDocxPath);
|
||||
workingPath = temporaryDocxPath;
|
||||
}
|
||||
|
||||
using (var document = WordprocessingDocument.Open(workingPath, false))
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var rootElements = GetRelevantRootElements(document);
|
||||
foreach (var root in rootElements)
|
||||
{
|
||||
AppendVisibleText(root, builder);
|
||||
if (builder.Length > 0 && builder[builder.Length - 1] != '\n')
|
||||
{
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return new ExtractedDocumentText
|
||||
{
|
||||
FilePath = filePath,
|
||||
VisibleText = builder.ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(temporaryDocxPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<OpenXmlPartRootElement> GetRelevantRootElements(WordprocessingDocument document)
|
||||
{
|
||||
var result = new List<KeyValuePair<string, OpenXmlPartRootElement>>();
|
||||
var partQueue = new Queue<OpenXmlPartContainer>();
|
||||
partQueue.Enqueue(document.MainDocumentPart);
|
||||
var visited = new HashSet<OpenXmlPart>();
|
||||
|
||||
while (partQueue.Count > 0)
|
||||
{
|
||||
var container = partQueue.Dequeue();
|
||||
foreach (var pair in container.Parts)
|
||||
{
|
||||
var part = pair.OpenXmlPart;
|
||||
if (part == null || !visited.Add(part))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.RootElement != null && IsRelevantPart(part))
|
||||
{
|
||||
result.Add(new KeyValuePair<string, OpenXmlPartRootElement>(GetStablePartKey(part), part.RootElement));
|
||||
}
|
||||
|
||||
partQueue.Enqueue(part);
|
||||
}
|
||||
}
|
||||
|
||||
var ordered = result
|
||||
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(item => item.Value)
|
||||
.ToList();
|
||||
|
||||
if (document.MainDocumentPart != null && document.MainDocumentPart.Document != null)
|
||||
{
|
||||
ordered.Insert(0, document.MainDocumentPart.Document);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private static bool IsRelevantPart(OpenXmlPart part)
|
||||
{
|
||||
return part is HeaderPart
|
||||
|| part is FooterPart
|
||||
|| part is WordprocessingCommentsPart
|
||||
|| part is FootnotesPart
|
||||
|| part is EndnotesPart
|
||||
|| part.GetType().Name.IndexOf("ChartPart", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static string GetStablePartKey(OpenXmlPart part)
|
||||
{
|
||||
return part == null || part.Uri == null
|
||||
? string.Empty
|
||||
: part.Uri.ToString();
|
||||
}
|
||||
|
||||
private static void AppendVisibleText(OpenXmlElement root, StringBuilder builder)
|
||||
{
|
||||
foreach (var element in root.Descendants())
|
||||
{
|
||||
var leaf = element as OpenXmlLeafTextElement;
|
||||
if (leaf != null)
|
||||
{
|
||||
if (!IsVisibleLeaf(leaf))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = leaf.Text;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(text);
|
||||
builder.Append(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!VmlTextMetadataUtility.IsVmlShapeElement(element))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var binding in VmlTextMetadataUtility.CreateBindings(element))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(binding.Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(binding.Text);
|
||||
builder.Append(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsVisibleLeaf(OpenXmlLeafTextElement leaf)
|
||||
{
|
||||
if (leaf is W.FieldCode || leaf is W.DeletedText)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (leaf.Ancestors<W.DeletedRun>().Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var run = leaf.Ancestors<W.Run>().FirstOrDefault();
|
||||
if (run != null && run.RunProperties != null && run.RunProperties.Vanish != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
DCIT.Core/Utilities/DisplayWidthUtility.cs
Normal file
103
DCIT.Core/Utilities/DisplayWidthUtility.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace DCIT.Core.Utilities
|
||||
{
|
||||
public static class DisplayWidthUtility
|
||||
{
|
||||
public static int Measure(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var width = 0;
|
||||
foreach (var character in value)
|
||||
{
|
||||
width += GetCharacterWidth(character);
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
public static string TruncateToWidth(string value, int maxWidth, out bool truncated)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || maxWidth <= 0)
|
||||
{
|
||||
truncated = !string.IsNullOrEmpty(value);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var width = 0;
|
||||
truncated = false;
|
||||
|
||||
foreach (var character in value)
|
||||
{
|
||||
var charWidth = GetCharacterWidth(character);
|
||||
if (width + charWidth > maxWidth)
|
||||
{
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
builder.Append(character);
|
||||
width += charWidth;
|
||||
}
|
||||
|
||||
if (!truncated && builder.Length < value.Length)
|
||||
{
|
||||
truncated = true;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static string PadToWidth(string value, int targetWidth, out bool padded)
|
||||
{
|
||||
var builder = new StringBuilder(value ?? string.Empty);
|
||||
var currentWidth = Measure(builder.ToString());
|
||||
padded = currentWidth < targetWidth;
|
||||
|
||||
while (currentWidth + 2 <= targetWidth)
|
||||
{
|
||||
builder.Append('\u3000');
|
||||
currentWidth += 2;
|
||||
}
|
||||
|
||||
while (currentWidth < targetWidth)
|
||||
{
|
||||
builder.Append(' ');
|
||||
currentWidth += 1;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static int GetCharacterWidth(char character)
|
||||
{
|
||||
if (character == '\u3000')
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (character >= 0x1100 &&
|
||||
(character <= 0x115F ||
|
||||
character == 0x2329 ||
|
||||
character == 0x232A ||
|
||||
(character >= 0x2E80 && character <= 0xA4CF && character != 0x303F) ||
|
||||
(character >= 0xAC00 && character <= 0xD7A3) ||
|
||||
(character >= 0xF900 && character <= 0xFAFF) ||
|
||||
(character >= 0xFE10 && character <= 0xFE19) ||
|
||||
(character >= 0xFE30 && character <= 0xFE6F) ||
|
||||
(character >= 0xFF00 && character <= 0xFF60) ||
|
||||
(character >= 0xFFE0 && character <= 0xFFE6)))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
270
DCIT.Core/Utilities/HashUtility.cs
Normal file
270
DCIT.Core/Utilities/HashUtility.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace DCIT.Core.Utilities
|
||||
{
|
||||
public static class HashUtility
|
||||
{
|
||||
private static readonly byte[] EncryptionKey = ComputeSha256Bytes("DCIT::DiffEncryptionKey::20260424");
|
||||
private static readonly byte[] TamperKey = ComputeSha256Bytes("DCIT::TamperProtectionKey::20260424");
|
||||
|
||||
public static string ComputeSha256Base64(byte[] bytes)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
return Convert.ToBase64String(sha.ComputeHash(bytes ?? new byte[0]));
|
||||
}
|
||||
}
|
||||
|
||||
public static string ComputeSha256Base64(string value)
|
||||
{
|
||||
return ComputeSha256Base64(Encoding.UTF8.GetBytes(value ?? string.Empty));
|
||||
}
|
||||
|
||||
public static string ComputeFileSha256Base64(string path)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
return Convert.ToBase64String(sha.ComputeHash(stream));
|
||||
}
|
||||
}
|
||||
|
||||
public static string ComputeHmacBase64(byte[] bytes)
|
||||
{
|
||||
using (var hmac = new HMACSHA256(TamperKey))
|
||||
{
|
||||
return Convert.ToBase64String(hmac.ComputeHash(bytes ?? new byte[0]));
|
||||
}
|
||||
}
|
||||
|
||||
public static string ComputeFileHmacBase64(string path)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
using (var hmac = new HMACSHA256(TamperKey))
|
||||
{
|
||||
return Convert.ToBase64String(hmac.ComputeHash(stream));
|
||||
}
|
||||
}
|
||||
|
||||
public static EncryptedPayload Encrypt(byte[] plaintextBytes)
|
||||
{
|
||||
var iv = new byte[16];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(iv);
|
||||
}
|
||||
|
||||
byte[] cipherBytes;
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Key = EncryptionKey;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using (var encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
cipherBytes = encryptor.TransformFinalBlock(plaintextBytes ?? new byte[0], 0, (plaintextBytes ?? new byte[0]).Length);
|
||||
}
|
||||
}
|
||||
|
||||
var tagInput = Combine(iv, cipherBytes);
|
||||
using (var hmac = new HMACSHA256(TamperKey))
|
||||
{
|
||||
return new EncryptedPayload
|
||||
{
|
||||
CiphertextBase64 = Convert.ToBase64String(cipherBytes),
|
||||
NonceBase64 = Convert.ToBase64String(iv),
|
||||
TagBase64 = Convert.ToBase64String(hmac.ComputeHash(tagInput)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static EncryptedPayloadHeader EncryptStream(Stream plaintextStream, Stream ciphertextStream)
|
||||
{
|
||||
if (plaintextStream == null)
|
||||
{
|
||||
throw new ArgumentNullException("plaintextStream");
|
||||
}
|
||||
|
||||
if (ciphertextStream == null)
|
||||
{
|
||||
throw new ArgumentNullException("ciphertextStream");
|
||||
}
|
||||
|
||||
if (!ciphertextStream.CanSeek)
|
||||
{
|
||||
throw new ArgumentException("密文流必须支持 Seek 以便计算防篡改标签。", "ciphertextStream");
|
||||
}
|
||||
|
||||
var iv = new byte[16];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(iv);
|
||||
}
|
||||
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Key = EncryptionKey;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using (var encryptor = aes.CreateEncryptor())
|
||||
using (var cryptoStream = new CryptoStream(ciphertextStream, encryptor, CryptoStreamMode.Write, leaveOpen: true))
|
||||
{
|
||||
plaintextStream.CopyTo(cryptoStream);
|
||||
cryptoStream.FlushFinalBlock();
|
||||
}
|
||||
}
|
||||
|
||||
ciphertextStream.Position = 0;
|
||||
using (var hmac = new HMACSHA256(TamperKey))
|
||||
{
|
||||
hmac.TransformBlock(iv, 0, iv.Length, iv, 0);
|
||||
var buffer = new byte[65536];
|
||||
int read;
|
||||
while ((read = ciphertextStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
hmac.TransformBlock(buffer, 0, read, buffer, 0);
|
||||
}
|
||||
|
||||
hmac.TransformFinalBlock(new byte[0], 0, 0);
|
||||
return new EncryptedPayloadHeader
|
||||
{
|
||||
NonceBase64 = Convert.ToBase64String(iv),
|
||||
TagBase64 = Convert.ToBase64String(hmac.Hash),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] Decrypt(string ciphertextBase64, string nonceBase64, string tagBase64)
|
||||
{
|
||||
var cipherBytes = Convert.FromBase64String(ciphertextBase64 ?? string.Empty);
|
||||
var iv = Convert.FromBase64String(nonceBase64 ?? string.Empty);
|
||||
var providedTag = Convert.FromBase64String(tagBase64 ?? string.Empty);
|
||||
var expectedTag = Convert.FromBase64String(ComputeHmacBase64(Combine(iv, cipherBytes)));
|
||||
if (!FixedTimeEquals(providedTag, expectedTag))
|
||||
{
|
||||
throw new CryptographicException("差异载荷验证失败。");
|
||||
}
|
||||
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Key = EncryptionKey;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
{
|
||||
return decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DecryptStream(Stream cipherStream, Stream plaintextStream, byte[] iv, byte[] expectedTag)
|
||||
{
|
||||
if (cipherStream == null)
|
||||
{
|
||||
throw new ArgumentNullException("cipherStream");
|
||||
}
|
||||
|
||||
if (plaintextStream == null)
|
||||
{
|
||||
throw new ArgumentNullException("plaintextStream");
|
||||
}
|
||||
|
||||
if (!cipherStream.CanSeek)
|
||||
{
|
||||
throw new ArgumentException("密文流必须支持 Seek 以便校验防篡改标签。", "cipherStream");
|
||||
}
|
||||
|
||||
using (var hmac = new HMACSHA256(TamperKey))
|
||||
{
|
||||
hmac.TransformBlock(iv ?? new byte[0], 0, (iv ?? new byte[0]).Length, iv ?? new byte[0], 0);
|
||||
cipherStream.Position = 0;
|
||||
var buffer = new byte[65536];
|
||||
int read;
|
||||
while ((read = cipherStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
hmac.TransformBlock(buffer, 0, read, buffer, 0);
|
||||
}
|
||||
|
||||
hmac.TransformFinalBlock(new byte[0], 0, 0);
|
||||
if (!FixedTimeEquals(hmac.Hash, expectedTag ?? new byte[0]))
|
||||
{
|
||||
throw new CryptographicException("差异载荷防篡改校验失败。");
|
||||
}
|
||||
}
|
||||
|
||||
cipherStream.Position = 0;
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Key = EncryptionKey;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
using (var cryptoStream = new CryptoStream(cipherStream, decryptor, CryptoStreamMode.Read, leaveOpen: true))
|
||||
{
|
||||
cryptoStream.CopyTo(plaintextStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ComputeSha256Bytes(string value)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(value ?? string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FixedTimeEquals(byte[] left, byte[] right)
|
||||
{
|
||||
if (left == null || right == null || left.Length != right.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var diff = 0;
|
||||
for (var index = 0; index < left.Length; index++)
|
||||
{
|
||||
diff |= left[index] ^ right[index];
|
||||
}
|
||||
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
private static byte[] Combine(byte[] left, byte[] right)
|
||||
{
|
||||
left = left ?? new byte[0];
|
||||
right = right ?? new byte[0];
|
||||
var bytes = new byte[left.Length + right.Length];
|
||||
Buffer.BlockCopy(left, 0, bytes, 0, left.Length);
|
||||
Buffer.BlockCopy(right, 0, bytes, left.Length, right.Length);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
public class EncryptedPayload
|
||||
{
|
||||
public string CiphertextBase64 { get; set; }
|
||||
|
||||
public string NonceBase64 { get; set; }
|
||||
|
||||
public string TagBase64 { get; set; }
|
||||
}
|
||||
|
||||
public class EncryptedPayloadHeader
|
||||
{
|
||||
public string NonceBase64 { get; set; }
|
||||
|
||||
public string TagBase64 { get; set; }
|
||||
}
|
||||
}
|
||||
60
DCIT.Core/Utilities/PathUtility.cs
Normal file
60
DCIT.Core/Utilities/PathUtility.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace DCIT.Core.Utilities
|
||||
{
|
||||
public static class PathUtility
|
||||
{
|
||||
public static string NormalizeDirectory(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(path.Trim());
|
||||
if (!fullPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
|
||||
{
|
||||
fullPath += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
public static bool AreSameOrNestedDirectories(string left, string right)
|
||||
{
|
||||
var normalizedLeft = NormalizeDirectory(left);
|
||||
var normalizedRight = NormalizeDirectory(right);
|
||||
if (string.IsNullOrEmpty(normalizedLeft) || string.IsNullOrEmpty(normalizedRight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalizedLeft.Equals(normalizedRight, StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedLeft.StartsWith(normalizedRight, StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedRight.StartsWith(normalizedLeft, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
var root = new Uri(NormalizeDirectory(relativeTo), UriKind.Absolute);
|
||||
var target = new Uri(Path.GetFullPath(path), UriKind.Absolute);
|
||||
var relative = root.MakeRelativeUri(target).ToString().Replace('/', Path.DirectorySeparatorChar);
|
||||
return Uri.UnescapeDataString(relative);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ValidateDirectoryPair(string firstLabel, string firstPath, string secondLabel, string secondPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(firstPath) || string.IsNullOrWhiteSpace(secondPath))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (AreSameOrNestedDirectories(firstPath, secondPath))
|
||||
{
|
||||
yield return string.Format("{0} 与 {1} 不能相同,也不能互为父子目录。", firstLabel, secondLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
342
DCIT.Core/Utilities/VmlTextMetadataUtility.cs
Normal file
342
DCIT.Core/Utilities/VmlTextMetadataUtility.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using DocumentFormat.OpenXml;
|
||||
|
||||
namespace DCIT.Core.Utilities
|
||||
{
|
||||
internal static class VmlTextMetadataUtility
|
||||
{
|
||||
private static readonly Regex Base64WhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled);
|
||||
|
||||
public static bool IsVmlShapeElement(OpenXmlElement element)
|
||||
{
|
||||
return element != null
|
||||
&& string.Equals(element.LocalName, "shape", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(element.NamespaceUri, "urn:schemas-microsoft-com:vml", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static IEnumerable<VmlTextBinding> CreateBindings(OpenXmlElement shape)
|
||||
{
|
||||
if (!IsVmlShapeElement(shape))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var binding in CreateEquationBindings(shape))
|
||||
{
|
||||
yield return binding;
|
||||
}
|
||||
|
||||
foreach (var binding in CreateChartBindings(shape))
|
||||
{
|
||||
yield return binding;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<VmlTextBinding> CreateEquationBindings(OpenXmlElement shape)
|
||||
{
|
||||
var attribute = FindAttribute(shape, "equationxml");
|
||||
if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var actualAttribute = attribute.Value;
|
||||
|
||||
XDocument document;
|
||||
try
|
||||
{
|
||||
var decoded = WebUtility.HtmlDecode(actualAttribute.Value);
|
||||
if (string.IsNullOrWhiteSpace(decoded))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
document = XDocument.Parse(decoded, LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
catch
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var textNodes = document
|
||||
.Descendants()
|
||||
.Where(item => string.Equals(item.Name.LocalName, "t", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
if (textNodes.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var textNode in textNodes)
|
||||
{
|
||||
var initialText = textNode.Value ?? string.Empty;
|
||||
if (initialText.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new VmlTextBinding
|
||||
{
|
||||
Text = initialText,
|
||||
IsEquationText = true,
|
||||
Apply = updatedText =>
|
||||
{
|
||||
textNode.Value = updatedText ?? string.Empty;
|
||||
SetAttribute(shape, actualAttribute, SerializeXml(document));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<VmlTextBinding> CreateChartBindings(OpenXmlElement shape)
|
||||
{
|
||||
var attribute = FindAttribute(shape, "gfxdata");
|
||||
if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var actualAttribute = attribute.Value;
|
||||
|
||||
ChartPackageContext context;
|
||||
try
|
||||
{
|
||||
context = ChartPackageContext.TryCreate(shape, actualAttribute);
|
||||
}
|
||||
catch
|
||||
{
|
||||
context = null;
|
||||
}
|
||||
|
||||
if (context == null || context.XmlEntries.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var entry in context.XmlEntries)
|
||||
{
|
||||
foreach (var textNode in entry.Document
|
||||
.Descendants()
|
||||
.Where(item => IsChartVisibleTextNode(item))
|
||||
.ToList())
|
||||
{
|
||||
var initialText = textNode.Value ?? string.Empty;
|
||||
if (initialText.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new VmlTextBinding
|
||||
{
|
||||
Text = initialText,
|
||||
IsChartText = true,
|
||||
Apply = updatedText =>
|
||||
{
|
||||
textNode.Value = updatedText ?? string.Empty;
|
||||
context.Save();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsChartVisibleTextNode(XElement element)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var localName = element.Name.LocalName;
|
||||
return string.Equals(localName, "t", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(localName, "v", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static OpenXmlAttribute? FindAttribute(OpenXmlElement element, string localName)
|
||||
{
|
||||
if (element == null || string.IsNullOrWhiteSpace(localName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var attribute in element.GetAttributes())
|
||||
{
|
||||
if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetAttribute(OpenXmlElement element, OpenXmlAttribute sourceAttribute, string value)
|
||||
{
|
||||
element.SetAttribute(new OpenXmlAttribute(
|
||||
sourceAttribute.Prefix,
|
||||
sourceAttribute.LocalName,
|
||||
sourceAttribute.NamespaceUri,
|
||||
value ?? string.Empty));
|
||||
}
|
||||
|
||||
private static string SerializeXml(XDocument document)
|
||||
{
|
||||
using (var writer = new Utf8StringWriter())
|
||||
using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings
|
||||
{
|
||||
OmitXmlDeclaration = document.Declaration == null,
|
||||
Encoding = new UTF8Encoding(false),
|
||||
Indent = false,
|
||||
NewLineHandling = NewLineHandling.None,
|
||||
}))
|
||||
{
|
||||
document.Save(xmlWriter);
|
||||
xmlWriter.Flush();
|
||||
return writer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ChartPackageContext
|
||||
{
|
||||
private readonly OpenXmlElement _shape;
|
||||
private readonly OpenXmlAttribute _attribute;
|
||||
private readonly List<EntryState> _allEntries;
|
||||
|
||||
private ChartPackageContext(OpenXmlElement shape, OpenXmlAttribute attribute, List<EntryState> allEntries, List<EntryState> xmlEntries)
|
||||
{
|
||||
_shape = shape;
|
||||
_attribute = attribute;
|
||||
_allEntries = allEntries;
|
||||
XmlEntries = xmlEntries;
|
||||
}
|
||||
|
||||
public List<EntryState> XmlEntries { get; private set; }
|
||||
|
||||
public static ChartPackageContext TryCreate(OpenXmlElement shape, OpenXmlAttribute attribute)
|
||||
{
|
||||
var decoded = WebUtility.HtmlDecode(attribute.Value ?? string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(decoded))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalizedBase64 = Base64WhitespaceRegex.Replace(decoded, string.Empty);
|
||||
if (normalizedBase64.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bytes = Convert.FromBase64String(normalizedBase64);
|
||||
var allEntries = new List<EntryState>();
|
||||
var xmlEntries = new List<EntryState>();
|
||||
|
||||
using (var input = new MemoryStream(bytes))
|
||||
using (var zip = new ZipArchive(input, ZipArchiveMode.Read, false))
|
||||
{
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
var state = new EntryState
|
||||
{
|
||||
FullName = entry.FullName,
|
||||
LastWriteTime = entry.LastWriteTime,
|
||||
};
|
||||
|
||||
using (var entryStream = entry.Open())
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
entryStream.CopyTo(memory);
|
||||
state.RawBytes = memory.ToArray();
|
||||
}
|
||||
|
||||
if (entry.FullName.IndexOf("drs/charts/", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& entry.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
state.Document = XDocument.Parse(
|
||||
Encoding.UTF8.GetString(state.RawBytes),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
catch
|
||||
{
|
||||
state.Document = null;
|
||||
}
|
||||
|
||||
if (state.Document != null)
|
||||
{
|
||||
xmlEntries.Add(state);
|
||||
}
|
||||
}
|
||||
|
||||
allEntries.Add(state);
|
||||
}
|
||||
}
|
||||
|
||||
return new ChartPackageContext(shape, attribute, allEntries, xmlEntries);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
using (var zip = new ZipArchive(output, ZipArchiveMode.Create, true))
|
||||
{
|
||||
foreach (var entry in _allEntries)
|
||||
{
|
||||
var targetEntry = zip.CreateEntry(entry.FullName, CompressionLevel.Optimal);
|
||||
targetEntry.LastWriteTime = entry.LastWriteTime;
|
||||
using (var targetStream = targetEntry.Open())
|
||||
{
|
||||
var bytes = entry.Document == null
|
||||
? (entry.RawBytes ?? Array.Empty<byte>())
|
||||
: Encoding.UTF8.GetBytes(SerializeXml(entry.Document));
|
||||
targetStream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetAttribute(_shape, _attribute, Convert.ToBase64String(output.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Utf8StringWriter : StringWriter
|
||||
{
|
||||
public override Encoding Encoding
|
||||
{
|
||||
get { return new UTF8Encoding(false); }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EntryState
|
||||
{
|
||||
public string FullName { get; set; }
|
||||
|
||||
public DateTimeOffset LastWriteTime { get; set; }
|
||||
|
||||
public byte[] RawBytes { get; set; }
|
||||
|
||||
public XDocument Document { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class VmlTextBinding
|
||||
{
|
||||
public string Text { get; set; }
|
||||
|
||||
public bool IsEquationText { get; set; }
|
||||
|
||||
public bool IsChartText { get; set; }
|
||||
|
||||
public Action<string> Apply { get; set; }
|
||||
}
|
||||
}
|
||||
BIN
DCIT.Core/bin/Debug/net48/DCIT.Core.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/DCIT.Core.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/DCIT.Core.pdb
Normal file
BIN
DCIT.Core/bin/Debug/net48/DCIT.Core.pdb
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.Framework.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.Framework.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/ExCSS.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/ExCSS.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/Newtonsoft.Json.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/Newtonsoft.Json.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/Svg.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/Svg.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/System.Buffers.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/System.Buffers.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/System.Memory.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/System.Memory.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net48/System.Numerics.Vectors.dll
Normal file
BIN
DCIT.Core/bin/Debug/net48/System.Numerics.Vectors.dll
Normal file
Binary file not shown.
Binary file not shown.
194
DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.deps.json
Normal file
194
DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.deps.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"DCIT.Core/1.0.0": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml": "3.5.1",
|
||||
"Newtonsoft.Json": "13.0.4",
|
||||
"Svg": "3.4.7"
|
||||
},
|
||||
"runtime": {
|
||||
"DCIT.Core.dll": {}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.5.1": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml.Framework": "3.5.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/DocumentFormat.OpenXml.dll": {
|
||||
"assemblyVersion": "3.5.1.0",
|
||||
"fileVersion": "3.5.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.5.1": {
|
||||
"dependencies": {
|
||||
"System.IO.Packaging": "8.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/DocumentFormat.OpenXml.Framework.dll": {
|
||||
"assemblyVersion": "3.5.1.0",
|
||||
"fileVersion": "3.5.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExCSS/4.2.3": {
|
||||
"runtime": {
|
||||
"lib/net6.0/ExCSS.dll": {
|
||||
"assemblyVersion": "4.2.3.0",
|
||||
"fileVersion": "4.2.3.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {},
|
||||
"Microsoft.Win32.SystemEvents/5.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": {
|
||||
"assemblyVersion": "5.0.0.0",
|
||||
"fileVersion": "5.0.20.51904"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.0",
|
||||
"fileVersion": "5.0.20.51904"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"runtime": {
|
||||
"lib/net6.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.4.30916"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Svg/3.4.7": {
|
||||
"dependencies": {
|
||||
"ExCSS": "4.2.3",
|
||||
"System.Drawing.Common": "5.0.3"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/Svg.dll": {
|
||||
"assemblyVersion": "3.4.0.0",
|
||||
"fileVersion": "3.4.7.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Drawing.Common/5.0.3": {
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.SystemEvents": "5.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"rid": "unix",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
},
|
||||
"runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"runtime": {
|
||||
"lib/net6.0/System.IO.Packaging.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"DCIT.Core/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.5.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-zxdOf5VVCe/uNklbRhj8dVBzQGj3DoqkUuqOp9cAZVuN8mNYDjof1lvSQA2OQNr8Ptc9d7pbA7Azq/ReaI3FpA==",
|
||||
"path": "documentformat.openxml/3.5.1",
|
||||
"hashPath": "documentformat.openxml.3.5.1.nupkg.sha512"
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.5.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==",
|
||||
"path": "documentformat.openxml.framework/3.5.1",
|
||||
"hashPath": "documentformat.openxml.framework.3.5.1.nupkg.sha512"
|
||||
},
|
||||
"ExCSS/4.2.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-SyeAfu2wL5247sipJoPUzQfjiwQtfSd8hN4IbgoyVcDx4PP6Dud4znwPRibWQzLtTlUxYYcbf5f4p+EfFC7KtQ==",
|
||||
"path": "excss/4.2.3",
|
||||
"hashPath": "excss.4.2.3.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==",
|
||||
"path": "microsoft.netcore.platforms/5.0.0",
|
||||
"hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Win32.SystemEvents/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==",
|
||||
"path": "microsoft.win32.systemevents/5.0.0",
|
||||
"hashPath": "microsoft.win32.systemevents.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==",
|
||||
"path": "newtonsoft.json/13.0.4",
|
||||
"hashPath": "newtonsoft.json.13.0.4.nupkg.sha512"
|
||||
},
|
||||
"Svg/3.4.7": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Omez7ly5BGhg3OzdV+LHZ5sI0+JQ6hF7WVKUeyHw4jRvcEWNCPCf1MWMBaf+R0DRBSZHx5EUHwBTEF+2oYtsAw==",
|
||||
"path": "svg/3.4.7",
|
||||
"hashPath": "svg.3.4.7.nupkg.sha512"
|
||||
},
|
||||
"System.Drawing.Common/5.0.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-rEQZuslijqdsO0pkJn7LtGBaMc//YVA8de0meGihkg9oLPaN+w+/Pb5d71lgp0YjPoKgBKNMvdq0IPnoW4PEng==",
|
||||
"path": "system.drawing.common/5.0.3",
|
||||
"hashPath": "system.drawing.common.5.0.3.nupkg.sha512"
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==",
|
||||
"path": "system.io.packaging/8.0.1",
|
||||
"hashPath": "system.io.packaging.8.0.1.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.dll
Normal file
BIN
DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.pdb
Normal file
BIN
DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.pdb
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/DCIT.Core.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/DCIT.Core.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/DCIT.Core.pdb
Normal file
BIN
DCIT.Core/bin/Release/net48/DCIT.Core.pdb
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.Framework.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.Framework.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/ExCSS.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/ExCSS.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/Newtonsoft.Json.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/Newtonsoft.Json.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/Svg.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/Svg.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/System.Buffers.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/System.Buffers.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/System.Memory.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/System.Memory.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net48/System.Numerics.Vectors.dll
Normal file
BIN
DCIT.Core/bin/Release/net48/System.Numerics.Vectors.dll
Normal file
Binary file not shown.
Binary file not shown.
194
DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.deps.json
Normal file
194
DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.deps.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"DCIT.Core/1.0.0": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml": "3.5.1",
|
||||
"Newtonsoft.Json": "13.0.4",
|
||||
"Svg": "3.4.7"
|
||||
},
|
||||
"runtime": {
|
||||
"DCIT.Core.dll": {}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.5.1": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml.Framework": "3.5.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/DocumentFormat.OpenXml.dll": {
|
||||
"assemblyVersion": "3.5.1.0",
|
||||
"fileVersion": "3.5.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.5.1": {
|
||||
"dependencies": {
|
||||
"System.IO.Packaging": "8.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/DocumentFormat.OpenXml.Framework.dll": {
|
||||
"assemblyVersion": "3.5.1.0",
|
||||
"fileVersion": "3.5.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExCSS/4.2.3": {
|
||||
"runtime": {
|
||||
"lib/net6.0/ExCSS.dll": {
|
||||
"assemblyVersion": "4.2.3.0",
|
||||
"fileVersion": "4.2.3.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {},
|
||||
"Microsoft.Win32.SystemEvents/5.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": {
|
||||
"assemblyVersion": "5.0.0.0",
|
||||
"fileVersion": "5.0.20.51904"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.0",
|
||||
"fileVersion": "5.0.20.51904"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"runtime": {
|
||||
"lib/net6.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.4.30916"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Svg/3.4.7": {
|
||||
"dependencies": {
|
||||
"ExCSS": "4.2.3",
|
||||
"System.Drawing.Common": "5.0.3"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/Svg.dll": {
|
||||
"assemblyVersion": "3.4.0.0",
|
||||
"fileVersion": "3.4.7.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Drawing.Common/5.0.3": {
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.SystemEvents": "5.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"rid": "unix",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
},
|
||||
"runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"runtime": {
|
||||
"lib/net6.0/System.IO.Packaging.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"DCIT.Core/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.5.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-zxdOf5VVCe/uNklbRhj8dVBzQGj3DoqkUuqOp9cAZVuN8mNYDjof1lvSQA2OQNr8Ptc9d7pbA7Azq/ReaI3FpA==",
|
||||
"path": "documentformat.openxml/3.5.1",
|
||||
"hashPath": "documentformat.openxml.3.5.1.nupkg.sha512"
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.5.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==",
|
||||
"path": "documentformat.openxml.framework/3.5.1",
|
||||
"hashPath": "documentformat.openxml.framework.3.5.1.nupkg.sha512"
|
||||
},
|
||||
"ExCSS/4.2.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-SyeAfu2wL5247sipJoPUzQfjiwQtfSd8hN4IbgoyVcDx4PP6Dud4znwPRibWQzLtTlUxYYcbf5f4p+EfFC7KtQ==",
|
||||
"path": "excss/4.2.3",
|
||||
"hashPath": "excss.4.2.3.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==",
|
||||
"path": "microsoft.netcore.platforms/5.0.0",
|
||||
"hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Win32.SystemEvents/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==",
|
||||
"path": "microsoft.win32.systemevents/5.0.0",
|
||||
"hashPath": "microsoft.win32.systemevents.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==",
|
||||
"path": "newtonsoft.json/13.0.4",
|
||||
"hashPath": "newtonsoft.json.13.0.4.nupkg.sha512"
|
||||
},
|
||||
"Svg/3.4.7": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Omez7ly5BGhg3OzdV+LHZ5sI0+JQ6hF7WVKUeyHw4jRvcEWNCPCf1MWMBaf+R0DRBSZHx5EUHwBTEF+2oYtsAw==",
|
||||
"path": "svg/3.4.7",
|
||||
"hashPath": "svg.3.4.7.nupkg.sha512"
|
||||
},
|
||||
"System.Drawing.Common/5.0.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-rEQZuslijqdsO0pkJn7LtGBaMc//YVA8de0meGihkg9oLPaN+w+/Pb5d71lgp0YjPoKgBKNMvdq0IPnoW4PEng==",
|
||||
"path": "system.drawing.common/5.0.3",
|
||||
"hashPath": "system.drawing.common.5.0.3.nupkg.sha512"
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==",
|
||||
"path": "system.io.packaging/8.0.1",
|
||||
"hashPath": "system.io.packaging.8.0.1.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.dll
Normal file
BIN
DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.pdb
Normal file
BIN
DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.pdb
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/DCIT.Core.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/DCIT.Core.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/DCIT.Core.pdb
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/DCIT.Core.pdb
Normal file
Binary file not shown.
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/ExCSS.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/ExCSS.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/Newtonsoft.Json.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/Newtonsoft.Json.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/Svg.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/Svg.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/System.Buffers.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/System.Buffers.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/System.Memory.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/System.Memory.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net48/System.Numerics.Vectors.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net48/System.Numerics.Vectors.dll
Normal file
Binary file not shown.
Binary file not shown.
194
DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.deps.json
Normal file
194
DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.deps.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"DCIT.Core/1.0.0": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml": "3.5.1",
|
||||
"Newtonsoft.Json": "13.0.4",
|
||||
"Svg": "3.4.7"
|
||||
},
|
||||
"runtime": {
|
||||
"DCIT.Core.dll": {}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.5.1": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml.Framework": "3.5.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/DocumentFormat.OpenXml.dll": {
|
||||
"assemblyVersion": "3.5.1.0",
|
||||
"fileVersion": "3.5.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.5.1": {
|
||||
"dependencies": {
|
||||
"System.IO.Packaging": "8.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/DocumentFormat.OpenXml.Framework.dll": {
|
||||
"assemblyVersion": "3.5.1.0",
|
||||
"fileVersion": "3.5.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExCSS/4.2.3": {
|
||||
"runtime": {
|
||||
"lib/net6.0/ExCSS.dll": {
|
||||
"assemblyVersion": "4.2.3.0",
|
||||
"fileVersion": "4.2.3.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {},
|
||||
"Microsoft.Win32.SystemEvents/5.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": {
|
||||
"assemblyVersion": "5.0.0.0",
|
||||
"fileVersion": "5.0.20.51904"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.0",
|
||||
"fileVersion": "5.0.20.51904"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"runtime": {
|
||||
"lib/net6.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.4.30916"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Svg/3.4.7": {
|
||||
"dependencies": {
|
||||
"ExCSS": "4.2.3",
|
||||
"System.Drawing.Common": "5.0.3"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/Svg.dll": {
|
||||
"assemblyVersion": "3.4.0.0",
|
||||
"fileVersion": "3.4.7.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Drawing.Common/5.0.3": {
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.SystemEvents": "5.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"rid": "unix",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
},
|
||||
"runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "5.0.0.2",
|
||||
"fileVersion": "5.0.1221.52207"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"runtime": {
|
||||
"lib/net6.0/System.IO.Packaging.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"DCIT.Core/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.5.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-zxdOf5VVCe/uNklbRhj8dVBzQGj3DoqkUuqOp9cAZVuN8mNYDjof1lvSQA2OQNr8Ptc9d7pbA7Azq/ReaI3FpA==",
|
||||
"path": "documentformat.openxml/3.5.1",
|
||||
"hashPath": "documentformat.openxml.3.5.1.nupkg.sha512"
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.5.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==",
|
||||
"path": "documentformat.openxml.framework/3.5.1",
|
||||
"hashPath": "documentformat.openxml.framework.3.5.1.nupkg.sha512"
|
||||
},
|
||||
"ExCSS/4.2.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-SyeAfu2wL5247sipJoPUzQfjiwQtfSd8hN4IbgoyVcDx4PP6Dud4znwPRibWQzLtTlUxYYcbf5f4p+EfFC7KtQ==",
|
||||
"path": "excss/4.2.3",
|
||||
"hashPath": "excss.4.2.3.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==",
|
||||
"path": "microsoft.netcore.platforms/5.0.0",
|
||||
"hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Win32.SystemEvents/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==",
|
||||
"path": "microsoft.win32.systemevents/5.0.0",
|
||||
"hashPath": "microsoft.win32.systemevents.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==",
|
||||
"path": "newtonsoft.json/13.0.4",
|
||||
"hashPath": "newtonsoft.json.13.0.4.nupkg.sha512"
|
||||
},
|
||||
"Svg/3.4.7": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Omez7ly5BGhg3OzdV+LHZ5sI0+JQ6hF7WVKUeyHw4jRvcEWNCPCf1MWMBaf+R0DRBSZHx5EUHwBTEF+2oYtsAw==",
|
||||
"path": "svg/3.4.7",
|
||||
"hashPath": "svg.3.4.7.nupkg.sha512"
|
||||
},
|
||||
"System.Drawing.Common/5.0.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-rEQZuslijqdsO0pkJn7LtGBaMc//YVA8de0meGihkg9oLPaN+w+/Pb5d71lgp0YjPoKgBKNMvdq0IPnoW4PEng==",
|
||||
"path": "system.drawing.common/5.0.3",
|
||||
"hashPath": "system.drawing.common.5.0.3.nupkg.sha512"
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==",
|
||||
"path": "system.io.packaging/8.0.1",
|
||||
"hashPath": "system.io.packaging.8.0.1.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.dll
Normal file
BIN
DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.pdb
Normal file
BIN
DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.pdb
Normal file
Binary file not shown.
77
DCIT.Core/obj/DCIT.Core.csproj.nuget.dgspec.json
Normal file
77
DCIT.Core/obj/DCIT.Core.csproj.nuget.dgspec.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj",
|
||||
"projectName": "DCIT.Core",
|
||||
"projectPath": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj",
|
||||
"packagesPath": "C:\\Users\\ly282\\.nuget\\packages\\",
|
||||
"outputPath": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\ly282\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0-windows"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0-windows7.0": {
|
||||
"targetAlias": "net6.0-windows",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0-windows7.0": {
|
||||
"targetAlias": "net6.0-windows",
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml": {
|
||||
"target": "Package",
|
||||
"version": "[3.5.1, )"
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"target": "Package",
|
||||
"version": "[13.0.4, )"
|
||||
},
|
||||
"Svg": {
|
||||
"target": "Package",
|
||||
"version": "[3.4.7, )"
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\6.0.428\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
DCIT.Core/obj/DCIT.Core.csproj.nuget.g.props
Normal file
15
DCIT.Core/obj/DCIT.Core.csproj.nuget.g.props
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\ly282\.nuget\packages\</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.3.4</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\ly282\.nuget\packages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
2
DCIT.Core/obj/DCIT.Core.csproj.nuget.g.targets
Normal file
2
DCIT.Core/obj/DCIT.Core.csproj.nuget.g.targets
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
|
||||
22
DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfo.cs
Normal file
22
DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfo.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// 由 MSBuild WriteCodeFragment 类生成。
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
be533eba519dee59447d07ad90b6f95f12e69288
|
||||
@@ -0,0 +1,8 @@
|
||||
is_global = true
|
||||
build_property.RootNamespace = DCIT.Core
|
||||
build_property.ProjectDir = D:\projects\DCIT\src\DCIT.Core\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
BIN
DCIT.Core/obj/Debug/net48/DCIT.Core.assets.cache
Normal file
BIN
DCIT.Core/obj/Debug/net48/DCIT.Core.assets.cache
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
ff20ad4232501c3bedafca0139d87ca3385ed275
|
||||
@@ -0,0 +1,43 @@
|
||||
D:\projects\DCIT\.build\verify-core-tests\DCIT.Core.dll
|
||||
D:\projects\DCIT\.build\verify-core-tests\DCIT.Core.pdb
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.AssemblyReference.cache
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfoInputs.cache
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfo.cs
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.CoreCompileInputs.cache
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.Up2Date
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.pdb
|
||||
D:\projects\DCIT\.build\verify-app-tests\DCIT.Core.dll
|
||||
D:\projects\DCIT\.build\verify-app-tests\DCIT.Core.pdb
|
||||
D:\projects\DCIT\.build\verify-app\DCIT.Core.dll
|
||||
D:\projects\DCIT\.build\verify-app\DCIT.Core.pdb
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.pdb
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.Framework.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\ExCSS.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\Newtonsoft.Json.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\Svg.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Buffers.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Memory.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Numerics.Vectors.dll
|
||||
D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Runtime.CompilerServices.Unsafe.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.pdb
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.Framework.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\ExCSS.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\Newtonsoft.Json.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\Svg.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Buffers.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Memory.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Numerics.Vectors.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Runtime.CompilerServices.Unsafe.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csprojAssemblyReference.cache
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfoInputs.cache
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfo.cs
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.CoreCompileInputs.cache
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.CopyComplete
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.dll
|
||||
E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.pdb
|
||||
0
DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.Up2Date
Normal file
0
DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.Up2Date
Normal file
Binary file not shown.
BIN
DCIT.Core/obj/Debug/net48/DCIT.Core.dll
Normal file
BIN
DCIT.Core/obj/Debug/net48/DCIT.Core.dll
Normal file
Binary file not shown.
BIN
DCIT.Core/obj/Debug/net48/DCIT.Core.pdb
Normal file
BIN
DCIT.Core/obj/Debug/net48/DCIT.Core.pdb
Normal file
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")]
|
||||
24
DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfo.cs
Normal file
24
DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfo.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")]
|
||||
[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")]
|
||||
|
||||
// 由 MSBuild WriteCodeFragment 类生成。
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
8c9305df27e0b0d043e93bc957ca166b518cd300
|
||||
@@ -0,0 +1,10 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net6.0-windows
|
||||
build_property.TargetPlatformMinVersion = 7.0
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = DCIT.Core
|
||||
build_property.ProjectDir = E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\
|
||||
BIN
DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.assets.cache
Normal file
BIN
DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.assets.cache
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
fed07599d218f093230e61661b1a9eee3dc52606
|
||||
@@ -0,0 +1,12 @@
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net6.0-windows\DCIT.Core.deps.json
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net6.0-windows\DCIT.Core.dll
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net6.0-windows\DCIT.Core.pdb
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.csproj.AssemblyReference.cache
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.AssemblyInfoInputs.cache
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.AssemblyInfo.cs
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.csproj.CoreCompileInputs.cache
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.dll
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\refint\DCIT.Core.dll
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.pdb
|
||||
E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\ref\DCIT.Core.dll
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user