first commit

This commit is contained in:
lihansani
2026-07-13 09:39:56 +08:00
commit 74e4738cfc
158 changed files with 15005 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"Bash(dotnet build *)",
"Bash(cmd //c dir /b/s \"e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本\\\\net6-0707\\\\tools\\\\DocTo\\\\\" 2>&1 | head -20)",
"Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本\\\\net6-0707\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length\")",
"Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本' | Select-Object Name\")",
"Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本\\\\net6-0707' -Directory | Select-Object Name\")",
"Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\打包\\\\Word文档\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length,LastWriteTime\")",
"Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\src\\\\DCIT.App\\\\bin\\\\Release\\\\net6.0-windows\\\\publish\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length,LastWriteTime\")",
"Bash(powershell -NoProfile -Command \"New-Item -ItemType Directory -Force -Path 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\tools\\\\DocTo' | Out-Null; Copy-Item -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\打包\\\\Word文档\\\\tools\\\\DocTo\\\\docto.exe' -Destination 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\tools\\\\DocTo\\\\docto.exe' -Force; Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length\")",
"Bash(dotnet publish *)",
"Bash(powershell -NoProfile -ExecutionPolicy Bypass -File \"e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\src\\\\_list_publish.ps1\")"
]
}
}

1
DCIT.App Submodule

Submodule DCIT.App added at f8f127a2c7

View 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>

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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
View 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
}
}

View 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));
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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); }
}
}
}

View 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; }
}
}
}

View 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; }
}
}
}

View 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();
}
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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; }
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

View 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;
}
}
}
}

View File

@@ -0,0 +1,7 @@
namespace DCIT.Core.Services
{
public interface IDocConversionService
{
void Convert(string sourcePath, string outputPath);
}
}

View File

@@ -0,0 +1,7 @@
namespace DCIT.Core.Services
{
public interface IDocumentFieldUpdateService
{
void UpdateDynamicContent(string documentPath);
}
}

View 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);
}
}

View File

@@ -0,0 +1,9 @@
namespace DCIT.Core.Services
{
public interface IFontValidationService
{
FontValidationResult Validate(string docxPath);
FontValidationResult EnsureFontsAvailable(string docxPath);
}
}

View 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));
}
}
}

View 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
{
}
}
}
}

View 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; }
}
}
}

View 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;
}
}
}

View 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>();
}
}

View 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
{
}
}
}
}

View 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;
}
}
}

View 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; }
}
}

View 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);
}
}
}
}

View 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; }
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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"
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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"
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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"
}
}
}

Binary file not shown.

Binary file not shown.

View 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"
}
}
}
}
}

View 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>

View 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" />

View File

@@ -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")]

View 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 类生成。

View File

@@ -0,0 +1 @@
be533eba519dee59447d07ad90b6f95f12e69288

View File

@@ -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 =

Binary file not shown.

View File

@@ -0,0 +1 @@
ff20ad4232501c3bedafca0139d87ca3385ed275

View File

@@ -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

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")]

View 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 类生成。

View File

@@ -0,0 +1 @@
8c9305df27e0b0d043e93bc957ca166b518cd300

View File

@@ -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\

Some files were not shown because too many files have changed in this diff Show More