1
1
using System . Formats . Tar ;
2
+ using System . IO . Compression ;
2
3
using System . Security . Cryptography ;
3
4
4
5
namespace Microsoft . NET . Build . Containers ;
@@ -26,37 +27,44 @@ public static Layer FromFiles(IEnumerable<(string path, string containerPath)> f
26
27
{
27
28
long fileSize ;
28
29
Span < byte > hash = stackalloc byte [ SHA256 . HashSizeInBytes ] ;
30
+ byte [ ] uncompressedHash ;
29
31
30
32
string tempTarballPath = ContentStore . GetTempFile ( ) ;
31
33
using ( FileStream fs = File . Create ( tempTarballPath ) )
32
34
{
33
- // using (GZipStream gz = new(fs, CompressionMode.Compress)) // TODO: https://github.com/dotnet/sdk-container-builds/issues/29
34
- using ( TarWriter writer = new ( fs , TarEntryFormat . Gnu , leaveOpen : true ) )
35
+ using ( HashDigestGZipStream gz = new ( fs , leaveOpen : true ) )
35
36
{
36
- foreach ( var item in fileList )
37
+ using ( TarWriter writer = new ( gz , TarEntryFormat . Gnu , leaveOpen : true ) )
37
38
{
38
- // Docker treats a COPY instruction that copies to a path like `/app` by
39
- // including `app/` as a directory, with no leading slash. Emulate that here.
40
- string containerPath = item . containerPath . TrimStart ( PathSeparators ) ;
39
+ foreach ( var item in fileList )
40
+ {
41
+ // Docker treats a COPY instruction that copies to a path like `/app` by
42
+ // including `app/` as a directory, with no leading slash. Emulate that here.
43
+ string containerPath = item . containerPath . TrimStart ( PathSeparators ) ;
44
+
45
+ writer . WriteEntry ( item . path , containerPath ) ;
46
+ }
47
+ } // Dispose of the TarWriter before getting the hash so the final data get written to the tar stream
41
48
42
- writer . WriteEntry ( item . path , containerPath ) ;
43
- }
49
+ uncompressedHash = gz . GetHash ( ) ;
44
50
}
45
51
46
52
fileSize = fs . Length ;
47
-
53
+
48
54
fs . Position = 0 ;
49
55
50
56
SHA256 . HashData ( fs , hash ) ;
51
57
}
52
58
53
59
string contentHash = Convert . ToHexString ( hash ) . ToLowerInvariant ( ) ;
60
+ string uncompressedContentHash = Convert . ToHexString ( uncompressedHash ) . ToLowerInvariant ( ) ;
54
61
55
62
Descriptor descriptor = new ( )
56
63
{
57
- MediaType = "application/vnd.docker.image.rootfs.diff.tar" , // TODO: configurable? gzip always?
64
+ MediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip " , // TODO: configurable? gzip always?
58
65
Size = fileSize ,
59
- Digest = $ "sha256:{ contentHash } "
66
+ Digest = $ "sha256:{ contentHash } ",
67
+ UncompressedDigest = $ "sha256:{ uncompressedContentHash } ",
60
68
} ;
61
69
62
70
string storedContent = ContentStore . PathForDescriptor ( descriptor ) ;
@@ -76,4 +84,74 @@ public static Layer FromFiles(IEnumerable<(string path, string containerPath)> f
76
84
77
85
private readonly static char [ ] PathSeparators = new char [ ] { Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar } ;
78
86
87
+ /// <summary>
88
+ /// A stream capable of computing the hash digest of raw uncompressed data while also compressing it.
89
+ /// </summary>
90
+ private sealed class HashDigestGZipStream : Stream
91
+ {
92
+ private readonly SHA256 hashAlgorithm ;
93
+ private readonly CryptoStream sha256Stream ;
94
+ private readonly Stream compressionStream ;
95
+
96
+ public HashDigestGZipStream ( Stream writeStream , bool leaveOpen )
97
+ {
98
+ hashAlgorithm = SHA256 . Create ( ) ;
99
+ sha256Stream = new CryptoStream ( Stream . Null , hashAlgorithm , CryptoStreamMode . Write ) ;
100
+ compressionStream = new GZipStream ( writeStream , CompressionMode . Compress , leaveOpen ) ;
101
+ }
102
+
103
+ public override bool CanWrite => true ;
104
+
105
+ public override void Write ( byte [ ] buffer , int offset , int count )
106
+ {
107
+ sha256Stream . Write ( buffer , offset , count ) ;
108
+ compressionStream . Write ( buffer , offset , count ) ;
109
+ }
110
+
111
+ public override void Write ( ReadOnlySpan < byte > buffer )
112
+ {
113
+ sha256Stream . Write ( buffer ) ;
114
+ compressionStream . Write ( buffer ) ;
115
+ }
116
+
117
+ public override void Flush ( )
118
+ {
119
+ sha256Stream . Flush ( ) ;
120
+ compressionStream . Flush ( ) ;
121
+ }
122
+
123
+ internal byte [ ] GetHash ( )
124
+ {
125
+ sha256Stream . FlushFinalBlock ( ) ;
126
+ return hashAlgorithm . Hash ! ;
127
+ }
128
+
129
+ protected override void Dispose ( bool disposing )
130
+ {
131
+ try
132
+ {
133
+ sha256Stream . Dispose ( ) ;
134
+ compressionStream . Dispose ( ) ;
135
+ }
136
+ finally
137
+ {
138
+ base . Dispose ( disposing ) ;
139
+ }
140
+ }
141
+
142
+ // This class is never used with async writes, but if it ever is, implement these overrides
143
+ public override Task WriteAsync ( byte [ ] buffer , int offset , int count , CancellationToken cancellationToken )
144
+ => throw new NotImplementedException ( ) ;
145
+ public override ValueTask WriteAsync ( ReadOnlyMemory < byte > buffer , CancellationToken cancellationToken )
146
+ => throw new NotImplementedException ( ) ;
147
+
148
+ public override bool CanRead => false ;
149
+ public override bool CanSeek => false ;
150
+ public override long Length => throw new NotImplementedException ( ) ;
151
+ public override long Position { get => throw new NotImplementedException ( ) ; set => throw new NotImplementedException ( ) ; }
152
+
153
+ public override int Read ( byte [ ] buffer , int offset , int count ) => throw new NotImplementedException ( ) ;
154
+ public override long Seek ( long offset , SeekOrigin origin ) => throw new NotImplementedException ( ) ;
155
+ public override void SetLength ( long value ) => throw new NotImplementedException ( ) ;
156
+ }
79
157
}
0 commit comments