Repository navigation
Expand file tree
/
Copy pathPdfSaveIntegrityTests.cs
More file actions
190 lines (167 loc) · 6.94 KB
/
Copy pathPdfSaveIntegrityTests.cs
File metadata and controls
190 lines (167 loc) · 6.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
using System;
using System.IO;
using System.Text;
using PdfEditorApp.Core.Utils;
using PdfEditorApp.Services.Tools.Core;
using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
using Xunit;
namespace PdfEditorApp.Tests;
/// <summary>
/// Regression tests for the Tier 1.5 save-path defects: the /Producer patch wrote one byte
/// past the span it replaced, and the patch ran in place over the file that had just been
/// reported as saved.
/// </summary>
[Collection("AppLogService")]
public class PdfSaveIntegrityTests
{
/// <summary>
/// Builds a minimal byte buffer containing a /Producer entry with the given value.
/// The sentinel after the closing paren is what the off-by-one used to clobber.
/// </summary>
private static byte[] BuildProducerBuffer(string existingProducer, out int sentinelIndex)
{
string prefix = "%PDF-1.4\n<< /Producer (";
string suffix = ") /Title (Keep Me) >>\n";
string text = prefix + existingProducer + suffix;
// The byte right after ')' — index of ')' + 1.
sentinelIndex = prefix.Length + existingProducer.Length + 1;
return Encoding.ASCII.GetBytes(text);
}
[Fact]
public void StringPadRight_NeverTruncates()
{
// The premise behind the fix: PadRight pads up to a width, it does not cut down to
// one. "%".PadRight(0) is "%", not "", so an exact-length replacement emitted one
// byte too many.
Assert.Equal("%", "%".PadRight(0, ' '));
Assert.Equal("% ", "%".PadRight(3, ' '));
}
[Fact]
public void PatchProducerInBytes_WithAnEqualLengthReplacement_DoesNotOverrunTheSpan()
{
// "codefrydev.in" is 13 chars, so an existing 13-char producer makes the
// replacement exactly as long as the span. That is the case where
// "%".PadRight(0) returned "%" and produced a one-byte overrun.
const string producer = "codefrydev.in";
var bytes = BuildProducerBuffer("aaaaaaaaaaaaa", out int sentinelIndex);
Assert.Equal(13, producer.Length);
byte sentinelBefore = bytes[sentinelIndex];
var result = PdfFileHelper.PatchProducerInBytes(bytes, producer);
Assert.Equal(sentinelBefore, result[sentinelIndex]);
Assert.Contains($"({producer})", Encoding.ASCII.GetString(result), StringComparison.Ordinal);
Assert.Contains("/Title (Keep Me)", Encoding.ASCII.GetString(result), StringComparison.Ordinal);
}
[Fact]
public void PatchProducerInBytes_WithAShorterReplacement_PadsExactly()
{
const string producer = "codefrydev.in";
var bytes = BuildProducerBuffer("a-much-longer-original-producer", out int sentinelIndex);
byte sentinelBefore = bytes[sentinelIndex];
int lengthBefore = bytes.Length;
var result = PdfFileHelper.PatchProducerInBytes(bytes, producer);
Assert.Equal(lengthBefore, result.Length);
Assert.Equal(sentinelBefore, result[sentinelIndex]);
Assert.Contains("/Title (Keep Me)", Encoding.ASCII.GetString(result), StringComparison.Ordinal);
}
[Fact]
public void PdfDocumentSanitizer_PatchProducer_HasTheSameBoundsBehaviour()
{
// The two implementations are verbatim copies; the fix has to hold in both.
const string producer = "codefrydev.in";
var bytes = BuildProducerBuffer("aaaaaaaaaaaaa", out int sentinelIndex);
byte sentinelBefore = bytes[sentinelIndex];
var result = PdfDocumentSanitizer.PatchProducerInBytes(bytes, producer);
Assert.Equal(sentinelBefore, result[sentinelIndex]);
Assert.Contains("/Title (Keep Me)", Encoding.ASCII.GetString(result), StringComparison.Ordinal);
}
// ─── Atomic save ────────────────────────────────────────────────────────
[Fact]
public void SaveDocumentWithFryPdfMetadata_ProducesAReadablePdf()
{
string path = Path.Combine(Path.GetTempPath(), $"save_{Guid.NewGuid():N}.pdf");
try
{
using (var doc = new PdfDocument())
{
var page = doc.AddPage();
using var gfx = XGraphics.FromPdfPage(page);
gfx.DrawString("Hello", new XFont("Arial", 12), XBrushes.Black, new XPoint(40, 40));
PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, path);
}
Assert.True(File.Exists(path));
using var reopened = PdfFileHelper.OpenDocumentSafely(path);
Assert.Equal(1, reopened.PageCount);
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}
[Fact]
public void SaveDocumentWithFryPdfMetadata_LeavesNoTempFileBehind()
{
string dir = Path.Combine(Path.GetTempPath(), $"savedir_{Guid.NewGuid():N}");
Directory.CreateDirectory(dir);
string path = Path.Combine(dir, "out.pdf");
try
{
using (var doc = new PdfDocument())
{
doc.AddPage();
PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, path);
}
Assert.Single(Directory.GetFiles(dir));
Assert.Empty(Directory.GetFiles(dir, "*.tmp"));
}
finally
{
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
}
}
[Fact]
public void SaveDocumentWithFryPdfMetadata_OverwritesAnExistingFileInPlace()
{
string path = Path.Combine(Path.GetTempPath(), $"overwrite_{Guid.NewGuid():N}.pdf");
try
{
using (var first = new PdfDocument())
{
first.AddPage();
PdfFileHelper.SaveDocumentWithFryPdfMetadata(first, path);
}
// File.Replace is used on the overwrite path; make sure it actually replaces.
using (var second = new PdfDocument())
{
second.AddPage();
second.AddPage();
second.AddPage();
PdfFileHelper.SaveDocumentWithFryPdfMetadata(second, path);
}
using var reopened = PdfFileHelper.OpenDocumentSafely(path);
Assert.Equal(3, reopened.PageCount);
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}
[Fact]
public void SaveDocumentWithFryPdfMetadata_CreatesMissingDirectories()
{
string dir = Path.Combine(Path.GetTempPath(), $"nested_{Guid.NewGuid():N}", "a", "b");
string path = Path.Combine(dir, "out.pdf");
try
{
using var doc = new PdfDocument();
doc.AddPage();
PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, path);
Assert.True(File.Exists(path));
}
finally
{
var root = Path.GetDirectoryName(Path.GetDirectoryName(dir));
if (root != null && Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
}