forked from tannerhelland/PhotoDemon
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pdPackager2.cls
2198 lines (1740 loc) · 127 KB
/
pdPackager2.cls
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
Persistable = 0 'NotPersistable
DataBindingBehavior = 0 'vbNone
DataSourceBehavior = 0 'vbNone
MTSTransactionMode = 0 'NotAnMTSObject
END
Attribute VB_Name = "pdPackageLegacyV2"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'***************************************************************************
'PhotoDemon "pdPackage" v2.0 Interface (e.g. Zip-like archive handler)
'Copyright 2014-2022 by Tanner Helland
'Created: 05/April/14
'Last updated: 25/February/20
'Last update: deprecate this class in favor of the new pdPackageChunky format (which is smaller,
' faster, and simpler). As part of deprecation, I have commented out many debug logging
' statements in this class. This class is extremely stable and I do not plan to debug it
' in the future; if this changes (oh I hope it doesn't) the debug statements can easily be
' reenabled.
'Dependencies: - pdStream class from photodemon.org (used to easily read/write memory and file buffers)
' - pdFSO class from photodemon.org (Unicode-friendly file operations)
' - pdStringStack class from photodemon.org (optimized handling of large string collections)
' - VBHacks module from photodemon.org (optimized workarounds for lacking VB functionality)
' - Compression module and any attached plugin modules (e.g. Plugin_zstd, Plugin_lz4) from photodemon.org
' (if you want compression support)
'
'This class provides an interface for creating and reading "pdPackage" files. pdPackages are zip-like archive files
' that contain one or more "nodes" (e.g. pieces of data), compressed or uncompressed, in a VB-friendly structure.
'
'v2.0 of this class is not backwards compatible with pdPackage v1.0. It is an entirely separate format, with many
' improvements over v1.0.
'
'Though I have created this class specifically for use with PhotoDemon, it should be easily usable by others
' with only minor modifications. Note that compression support requires an explicit path to libdeflate,
' zstandard (zstd), or lz4/lz4-hc, with the expected filename of "libdeflate.dll", "libzstd.dll", or "liblz4.dll",
' respectively. This class can also be used without compression, though its benefits are greatly reduced.
' (And obviously, you will not be able to open compressed archives from other sources.)
'
'While the files created by this class have many similarities to ZIP files, THEY ARE NOT ACTUAL ZIP FILES,
' and this class cannot read or write ZIP files. Files from this class are, by design, much simpler than
' ZIP files, and their structure and layout plays better with VB's inherent limitations.
'
'Key features include:
'
' 1) Data agnosticism, e.g. everything is treated as byte arrays, so you can store whatever data you'd like.
' 2) Very small front-loaded header, and rear-loaded directory. A small front-loaded header allows for quick file
' validation, but like zip files, the full directory exists at the header of the archive. This allows us to
' write pdPackages "as we go", which greatly improves both memory usage and performance.
' 3) Fixed-width directory entries. This allows the entire archive directory to be read in a single operation,
' rather than manually parsing variable-width directory entries until all have been located.
' 4) Support for compression on a per-node basis.
' 5) Support for two data "entries" per node, typically a header chunk and an actual data chunk. These two structs
' don't need to be used (one or the other or neither is just fine), but I greatly appreciate the simplicity of
' storing two pieces of data per node. For example, when using pdPackage's automatic file and folder compression
' features, the "header" node stores the original filename for each file, while the "data" node stores the file
' itself. This makes it easy to iterate filenames without touching full file contents.
' 6) A per-node access system, including compression, so that you can easily extract a single node without needing to
' decompress the entire archive. Also, this allows you to use different compression settings for each node
' (e.g. not everything needs to be compressed - only the bits you find relevant).
'
'Here are a few things to note if you want to use this class in your own projects:
'
' 1) At present, pdPackage files are not easily editable. Once created, there is no interface for adding new nodes,
' erasing existing nodes, or modifying any of the pdPackage settings (compression, etc). There's nothing in the format
' design that prevents these actions, but I haven't written edit functions because I have no reason to do so in PhotoDemon.
' 2) As noted above, external libraries are required for compression. pdPackages are designed in a way that makes it easy
' to use any compression functions (or other modification functions, e.g. encryption) of your choosing, or to ignore
' compression entirely if you don't require it. That said, if you want to use the class without a compression library,
' but you intend to work with pdPackage files from other sources (which may use compression), you need to make use of
' the GetPackageFlag() function and the accompanying PDPF2_(library-name)_REQUIRED flags. These will identify files
' your software cannot process without compression libraries.
' 3) Up to 2GB of data is theoretically supported, but you won't be able to reach that amount from within VB. The
' rear-loaded directory format makes it possible to support larger file sizes, but I have not yet done the work to
' implement this. Sorry. (Patches welcome!)
' 4) When reading pdPackage files, relevant file bits will only be loaded as-necessary. As such, you cannot delete the
' original file until all interactions are complete.
'
'Unless otherwise noted, all source code in this file is shared under a simplified BSD license.
' Full license details are available in the LICENSE.md file, or at https://photodemon.org/license/
'
'***************************************************************************
Option Explicit
'This constant should be updated whenever the core assembly/disassembly code is modified. This value is embedded in all
' pdPackage files written by this class, as a failsafe against potential ABI breakage in the future.
' - The current expected value is 70. Versions 66 and previous require the original pdPackage v1.0 class. This class
' *does not load pdPackage versions prior to v70*, by design.
' - The constants between 67 and 69, inclusive, have no meaning and are considered invalid.
' - The lowest supported value for this constant is 70, representative of PhotoDemon 7.0, when the PDI format was
' overhauled to support a bunch of new features (like zstd compression).
Private Const THIS_PDPACKAGE_VERSION As Long = 70
'The first four bytes of a valid pdPackage file must always be &H4B504450 ("PDPK" in ASCII, if viewed in a text editor
' - note the little-endian declaration here). Note that this constant is shared with the old pdPackage v1.0 format.
Private Const PDP_UNIVERSAL_IDENTIFIER As Long = &H4B504450
'Because pdPackage is awesome, it can store some complicated data structures automatically, with no work on the caller's part.
' Structures like this are identified with special internal names, which simplifies the extraction process.
Private Const PDP_DATA_FILE As String = "PDPFILE"
Private Const PDP_DATA_FILEANDPATH As String = "PDPFILE_AND_PATH"
Private Enum PDP_SPECIAL_NODE_TYPE
PDP_SN_Filename = 0
PDP_SN_FilenameAndRelativePath = 1
End Enum
#If False Then
Private Const PDP_SN_Filename = 0, PDP_SN_FilenameAndRelativePath = 1
#End If
'Each pdPackage file has a short file header. This header is separate from the directory chunk, and it contains all information
' necessary to prepare a node directory array.
Private Type PDP_HEADER
PDP_ID As Long 'pdPackage Identifier; must always be &H4B504450 ("PDPK" in ASCII)
PDP_SubID As Long 'pdPackage sub-identifier. This can be used by callers to define a specific type of pdPackage.
' (For example, PD uses this to denote PDI files specifically.) This value has no intrinsic meaning
' to this class.
PDP_Version As Long 'Version number of the pdPackage class used to write this file. All parsing behavior is based on this
' value, and it is automatically set according to the THIS_PDPACKAGE_VERSION constant at the top of
' this class. (Note that you *must* check this version number manually if you want to support the old
' pdPackage 1.0 format. This class does not handle versions prior to 70.)
NodeCount As Long 'Number of data nodes in this package, 1-based (e.g. if there is one node in the archive, this value will
' be set to 1). Cannot currently be zero, FYI.
NodeStructSize As Long 'Size of an individual node struct. This can be used as a failsafe check against PDP_Version, above.
DirectoryChunkPosition As Long 'Position of the first byte in the directory byte stream, stored as an absolute value from position 0
' (e.g. this value will *always* be larger than the size of this header struct, and the size of this
' package's data chunk).
DirectoryChunkSizePacked As Long 'Size of the directory chunk bytestream, as stored in the file. If the directory chunk is compressed,
' this value will be different from DirectoryChunkSizeFinal, below.
DirectoryChunkSizeOriginal As Long 'Size of the full node directory structure, including all node directory entries, when uncompressed.
' You can compare this value to DirectoryChunkSizeStored to determine if compression was applied.
DirectoryFlags(0 To 2) As Long 'User-defined flags for the directory chunk. See the PDP_FLAGS_V2 enum for details.
DataChunkPosition As Long 'Position of the first byte in the data byte stream, stored as an absolute value from position 0
' (e.g. this value will *always* be larger than the size of this header struct).
DataChunkSizePacked As Long 'Size of the data chunk bytestream, as stored in this package. This could be inferred by calculating
' the difference between the end of the file header and the start of the directory chunk, but it's
' easier to simply note it right inside the header.
DataChunkSizeOriginal As Long 'Size of the data chunk bytestream post-decompression. At present, this will always be identical to
' the DataChunkSizeStored value, as compression is not currently supported for the entire data chunk.
DataFlags(0 To 2) As Long 'User-defined flags for the data chunk. See the PDP_FLAGS_V2 enum for details.
Reserved As Long 'Reserved for future use; no relevance at present.
End Type
'The number of flags PD supports is relatively thin at present. This is by design, as I don't want to use them for trivial shit.
' Note that in an effort to keep flags as simple as possible, they are referred to by bit ordinal position [0, 31] rather than raw hex value.
'Also note that there are two sets of flags: one set for the file header (which describes the file as a whole, e.g. the ZLibRequired flag means
' "something in this file requires ZLib to be present), and a second set for individual nodes (which describe *just* that node,
' e.g. the ZlibRequiredHeader flag means "this node's header chunk requires ZLib for decompression").
'FYI: flags can be stored in the directory and data chunks, as well as individual nodes. When checking flags (e.g. after a load operation),
' the caller can specify which flag location they want checked.
Public Enum PDP_FLAG_SOURCE
PDP_FS_Any = -1
PDP_FS_Directory = 0
PDP_FS_Data = 1
PDP_FS_IndividualNode = 2
End Enum
#If False Then
Private Const PDP_FS_Any = -1, PDP_FS_Directory = 0, PDP_FS_Data = 1, PDP_FS_IndividualNode = 2
#End If
'The list of currently supported flags is as follows:
Public Enum PDP_HEADER_FLAGS_V2
PDP_HF2_FileCollection = 0 'This file contains zip-like file+folder entries created from the AutoAddNode(s) function(s).
PDP_HF2_ZlibRequired = 1 'There are one or more compressed entries in this chunk, so zLib will be required to read it
PDP_HF2_ZstdRequired = 2 'There are one or more zstd-compressed entries in this chunk
PDP_HF2_Lz4Required = 3 'There are one or more lz4-compressed entries in this chunk. Note that this flag doubles for both LZ4 and LZ4-HC, as they use identical decompressors.
End Enum
#If False Then
Private Const PDP_HF2_ZlibRequired = 0, PDP_HF2_FileCollection = 1, PDP_HF2_ZstdRequired = 2, PDP_HF2_Lz4Required = 3
#End If
Public Enum PDP_NODE_FLAGS_V2
PDP_NF2_FileNode = 0 'This node contains a file added via the shortcut AutoAddNodeFromFile() function.
PDP_NF2_ZlibRequiredH = 1 'There are one or more compressed entries in this chunk, so zLib will be required to read it
PDP_NF2_ZstdRequiredH = 2 'There are one or more zstd-compressed entries in this chunk
PDP_NF2_Lz4RequiredH = 3 'There are one or more lz4-compressed entries in this chunk. Note that this flag doubles for both LZ4 and LZ4-HC, as they use identical decompressors.
PDP_NF2_ZlibRequired = 4 'There are one or more compressed entries in this chunk, so zLib will be required to read it
PDP_NF2_ZstdRequired = 5 'There are one or more zstd-compressed entries in this chunk
PDP_NF2_Lz4Required = 6 'There are one or more lz4-compressed entries in this chunk. Note that this flag doubles for both LZ4 and LZ4-HC, as they use identical decompressors.
End Enum
#If False Then
Private Const PDP_NF2_FileNode = 0, PDP_NF2_ZlibRequiredH = 1, PDP_NF2_ZstdRequiredH = 2, PDP_NF2_Lz4RequiredH = 3, PDP_NF2_ZlibRequired = 4, PDP_NF2_ZstdRequired = 5, PDP_NF2_Lz4Required = 6
#End If
'Immediately following the PDP_HEADER is the data chunk, and following the data chunk is the data directory. The directory
' is comprised of FileHeader.NodeCount individual PDP_NODE structs. These structs are small and flexible, and *they have a
' fixed size*, meaning they can be read into a fixed-width array in a single pass.
Private Type PDP_NODE
nodeName As String * 32 'Name of the node, as a standard VB String (DBCS). It is thus 64 bytes long.
' Note that node name has no special meaning to this class; it is up to the caller to make
' the name meaningful.
NodeID As Long 'Alternatively, calling functions can specify an optional 4-byte numerical ID. Nodes can be
' read by 32-char name or 4-byte ID; this decision is up to the caller.
OptionalNodeType As Long 'Calling functions can also assign each node a 4-byte TYPE identifier if they want. This value
' has no intrinsic meaning to this class.
NodeFlags(0 To 3) As Long '16 bytes of node-specific flags are allowed. At present, these are mostly unused.
'One of the unique features of pdPackages is that each node is allotted two entries in the data chunk. These entries don't have
' to be used; in fact, neither has to be used, but they can be helpful for reading node-specific information without having to
' decode the entire node contents. (For example, when using the automatic file compression features, this class stores filenames
' inside the header entry, and the file contents inside the data entry. This lets us extract the list of files very efficiently.)
NodeHeaderOffset As Currency 'Absolute offset of this node's header in the data chunk, STARTING FROM THE START OF THE DATA CHUNK, not the start of the file!
NodeHeaderPackedSize As Currency 'Packed size of this node's header chunk. (This is the size the node's header array occupies in the pdPackage data chunk.)
NodeHeaderOriginalSize As Currency 'Original size of this node's header chunk. (If this value is the same as NodeHeaderPackedSize, the node header was stored uncompressed.)
NodeDataOffset As Currency 'Absolute offset of this node's data in the data chunk, STARTING FROM THE START OF THE DATA CHUNK, not the start of the file!
NodeDataPackedSize As Currency 'Packed size of this node's data chunk. (This is the size the node's data array occupies in the pdPackage data chunk.)
NodeDataOriginalSize As Currency 'Original size of this node's data chunk. (If this value is the same as NodeHeaderPackedSize, the node data was stored uncompressed.)
End Type
'When writing new pdPackage files, these variables hold the package's contents as they are being assembled. When loading a pdPackage file,
' these structs will be filled after the file header is successfully verified.
Private m_FileHeader As PDP_HEADER
Private m_NodeDirectory() As PDP_NODE
Private m_numOfNodes As Long
'The actual data chunk of the pdPackage is assembled using a pdStream instance. It greatly simplifies the process of
' assembling a 1D byte array from discrete individual chunks.
Private m_DataBuffer As pdStream
'As of v2, pdPackages can be wrapped directly around source files, which greatly reduces memory usage compared to assembling
' everything in-memory. The access mode of the current package instance (including read/write access) is stored here.
Private m_StreamMode As PD_STREAM_MODE, m_StreamAccess As PD_STREAM_ACCESS
'Certain modes may require us to use temporary buffers for compression/decompression. We try to use this as little as possible,
' but sometimes it's inevitable.
Private m_CompressionBuffer() As Byte
'File operations are made easier by using the pdFSO class, which wraps a bunch of Unicode-friendly file APIs
Private m_File As pdFSO
'pdPackage operations are roughly divided into two groups:
'
' - GET operations, for retrieving data from existing pdPackage files. GET operations include:
' readPackageFromFile, getNodeInfo, getNodeDataByID/Index/Name, getPackageFlag
'
' - SET operations, for creating new pdPackage files. SET operations include:
' prepareNewPackage, addNode, addNodeData, writePackageToFile
'
'At present, these two types of operations do not interact reliably, meaning you cannot use SET operations to modify a
' pdPackage you have loaded using GET operations. Packages must be created and written to file in one fell swoop, and
' if you want to read a pdPackage, I strongly recommend creating a dedicated class for just that file (due to the way
' this class caches file contents).
'Before creating a pdPackage file, you must call this function once. It preps all internal structures in anticipation of
' data loading. If you know the number of data nodes you will be writing, you can mention it in advance, which makes the
' directory assembly process much faster (because we don't have to ReDim Preserve the directory when we run out of space).
' Similarly, if you have some notion of how large the data chunk will be, you can supply it in advance. There is generally
' no penalty to over-estimating space, as the buffer will be trimmed before writing it out to file.
Friend Sub PrepareNewPackage(Optional ByVal numOfDataNodes As Long = 0, Optional ByVal optPackageID As Long = 0, Optional ByVal estimatedDataChunkSize As Long = 0, Optional ByVal streamMode As PD_STREAM_MODE = PD_SM_MemoryBacked, Optional ByVal backingFilename As String = vbNullString)
'Reset all module-level storage structs related to writing a new pdPackage file
'Start by preparing the file header. This will be updated before being written out to file, but we can set certain
' items in advance.
With m_FileHeader
.PDP_ID = PDP_UNIVERSAL_IDENTIFIER
.PDP_SubID = optPackageID
.PDP_Version = THIS_PDPACKAGE_VERSION
.NodeCount = numOfDataNodes
'Retrieve the size of a node struct. This value should never change, but never is a long time, and this gives us
' a failsafe against things like mismatched PDP version numbers. It also makes it easier for external load functions
' to know how to size the directory structure array.
Dim tmpNode As PDP_NODE
.NodeStructSize = LenB(tmpNode)
'DirectoryChunkSize can be assumed from the numOfDataNodes, but note that it will be verified again before the data is
' actually written out to file.
.DirectoryChunkSizeOriginal = .NodeStructSize * numOfDataNodes
.DirectoryChunkSizePacked = 0
'DirectoryFlags() and DataFlags() are set by separate functions. For now, assume a value of 0 for all flags.
Dim i As Long
For i = 0 To UBound(.DirectoryFlags)
.DirectoryFlags(i) = 0
.DataFlags(i) = 0
Next i
'DataChunkSize will remain unknown until all nodes have been added.
.DataChunkSizeOriginal = 0
.DataChunkSizePacked = 0
'4 bytes are reserved at the end as a "just in case". For now, they should always be 0.
.Reserved = 0
End With
'Resize the directory array to the number of supplied nodes; note that this step is optional; if no node count is supplied,
' the AddNode() function will automatically increment itself as necessary. (We also cover the case where this array already
' exists from prior writes; we can skip initialization in that case, to save a bit of memory thrashing.)
If (m_numOfNodes > 0) Then
If (m_numOfNodes < numOfDataNodes) Then ReDim m_NodeDirectory(0 To numOfDataNodes) As PDP_NODE
Else
ReDim m_NodeDirectory(0 To numOfDataNodes) As PDP_NODE
End If
m_numOfNodes = 0
'Prepare the data buffer. Note that chunk size is largely irrelevant for our purposes; when handed a byte array that exceeds
' the size of the default chunk size, the buffer class is smart enough to extend the buffer by the size of that byte array.
' If writing a lot of small data, however, the chunk size becomes important; try to make it large enough to require minimal
' ReDim Preserve requests throughout the pdPackage assembly process.
If (m_DataBuffer Is Nothing) Then
Set m_DataBuffer = New pdStream
Else
If (m_StreamMode <> streamMode) Then m_DataBuffer.StopStream True
End If
m_StreamMode = streamMode
If (streamMode = PD_SM_MemoryBacked) Then
m_DataBuffer.StartStream PD_SM_MemoryBacked, PD_SA_ReadWrite, , estimatedDataChunkSize, , , True
'In file-backed mode, we'll actually open the file right away, and write our nodes to it "as we go"
ElseIf (streamMode = PD_SM_FileBacked) Then
m_DataBuffer.StartStream PD_SM_FileBacked, PD_SA_ReadWrite, backingFilename, estimatedDataChunkSize, LenB(m_FileHeader), OptimizeSequentialAccess
End If
End Sub
'Add a new node to this pdPackage instance. Note that this function DOES NOT actually add the node's data arrays to the main
' data buffer - those are done in a subsequent step, by the user, as necessary. (It's a little pesky to separate node additions
' into multiple steps, but this allows for more fine-grained control over node addition, without overwhelming any function with
' a monstrous list of parameters.)
'
'Importantly, this function returns the index of the added node, which external functions must then use to supply this node's
' actual data arrays.
Friend Function AddNode(Optional ByVal thisNodeName As String = vbNullString, Optional ByVal thisNodeID As Long = 0, Optional ByVal thisNodeType As Long = 0) As Long
'Increment our active node count
m_numOfNodes = m_numOfNodes + 1
'Start by making sure our node directory is large enough to hold this new node (if the caller supplied a node count up front,
' this step is overkill). If the directory is too small, enlarge it.
If (UBound(m_NodeDirectory) < (m_numOfNodes - 1)) Then
'If the array has never been resized before, give it a nice starting size of 16 entries
If (UBound(m_NodeDirectory) = 0) Then
ReDim Preserve m_NodeDirectory(0 To 15) As PDP_NODE
'If the directory has been resized before, double its size now. (The directory will automatically be shrunk to minimum
' size when it's written out to file, so don't worry about excess space being allocated.)
Else
ReDim Preserve m_NodeDirectory(0 To UBound(m_NodeDirectory) * 2 + 1) As PDP_NODE
End If
End If
'Copy the supplied values into the node directory. Note that all three ID types are optional, but hopefully the user has
' been smart enough to make use of at least one of them!
With m_NodeDirectory(m_numOfNodes - 1)
.nodeName = thisNodeName
.NodeID = thisNodeID
.OptionalNodeType = thisNodeType
End With
'Return the index of this node, which the caller will use to supply this node's data (in a separate step).
AddNode = m_numOfNodes - 1
End Function
'Add data for a given node. Four required params, three optional params. The function will return TRUE if successful.
'
' The four required params are:
' 1) Index of the target node. This is the value returned by AddNode(), above.
' 2) Destination chunk for this data. Remember that each node in a pdPackage supports TWO possible data arrays, which can be
' utilized however the caller pleases. (Typically, one is used for lightweight header data, while the other is used for
' heavy content data.)
' 3, 4) Pointer and size of the byte array containing the data the caller wants written. This class doesn't care how the
' pointer is obtained or assembled, so long as the SIZE IS BYTE-ACCURATE.
'
' The three optional params are:
' 1) Whether to compress the data before writing it. If zLib is unavailable, this param has no meaning, as data will always
' be written without compression.
' 2) Compression level. The meaning of this varies depending on the compression engine in use. zLib allows a value from
' 0 (uncompressed) to 9 (best possible compression). zstd allows a value from 1 (fast, leaner compression) to 22 (slow,
' very intense compression). The default value of -1 means "use default compression", and PD will choose an appropriate
' value for the given library.
Friend Function AddNodeDataFromPointer(ByVal nodeIndex As Long, ByVal useHeaderBuffer As Boolean, ByVal dataPointer As Long, ByVal dataLength As Long, Optional ByVal compressData As PD_CompressionFormat = cf_Zstd, Optional ByVal compressionLevel As Long = -1) As Boolean
'Start by validating the node index we were passed. If it's out of range, exit immediately.
If (nodeIndex < 0) Or (nodeIndex > m_numOfNodes - 1) Then
'InternalErrorMsg "Node index out of range - try again with a valid node index!"
AddNodeDataFromPointer = False
Exit Function
End If
'If compression is requested, make sure a matching compression engine is available
If ((compressData = cf_Zlib) And (Not Compression.IsFormatSupported(cf_Zlib))) Then compressData = cf_None
If ((compressData = cf_Zstd) And (Not Compression.IsFormatSupported(cf_Zstd))) Then compressData = cf_None
If (((compressData = cf_Lz4) Or (compressData = cf_Lz4hc)) And (Not Compression.IsFormatSupported(cf_Lz4))) Then compressData = cf_None
'If compression is requested, update the "[compression name] required" flag for both the file as a whole, and this specific node.
If (compressData <> cf_None) Then SetCompressionFlag nodeIndex, useHeaderBuffer, compressData, True, True
'Update the pre-compression values for this data chunk.
If useHeaderBuffer Then
m_NodeDirectory(nodeIndex).NodeHeaderOriginalSize = dataLength
Else
m_NodeDirectory(nodeIndex).NodeDataOriginalSize = dataLength
End If
'Mark the offset for this data chunk.
If useHeaderBuffer Then
m_NodeDirectory(nodeIndex).NodeHeaderOffset = m_DataBuffer.GetPosition
Else
m_NodeDirectory(nodeIndex).NodeDataOffset = m_DataBuffer.GetPosition
End If
'If we are not using compression, we can write the node data as-is. Note that header or data doesn't matter here,
' because the data is simply added to the pdPackages data chunk. Order of writing is irrelevant.
If (compressData = cf_None) Then
m_DataBuffer.WriteBytesFromPointer dataPointer, dataLength
If useHeaderBuffer Then
m_NodeDirectory(nodeIndex).NodeHeaderPackedSize = dataLength
Else
m_NodeDirectory(nodeIndex).NodeDataPackedSize = dataLength
End If
'Data compression was requested, meaning we have a bit of extra work to do.
Else
Dim compressedSize As Long, compressionIsAnImprovement As Boolean
'Because this code is time-sensitive in PD, I like to track and report performance
Dim startTime As Currency
VBHacks.GetHighResTime startTime
'copyPtrToArray will return TRUE if the data was compressed; FALSE if it was not. There is no error state for the function.
compressionIsAnImprovement = CopyPtrToArray(dataPointer, dataLength, m_CompressionBuffer, compressedSize, compressData, compressionLevel, False)
'Perform a failsafe check to ensure that the compressed size is smaller than the uncompressed size
If compressionIsAnImprovement Then compressionIsAnImprovement = (compressedSize < dataLength)
'If compression was successful (and the end result is smaller than the original), point the source data pointer
' at the compressed data.
Dim srcDataPointer As Long
If compressionIsAnImprovement Then
srcDataPointer = VarPtr(m_CompressionBuffer(0))
'Debug.Print "Node data compressed in " & VBHacks.GetTimeDiffNowAsString(startTime) & "; size reduction is " & Format$(100# - (100# * (CDbl(compressedSize) / CDbl(dataLength))), "#0.0") & "% smaller (" & dataLength & " to " & compressedSize & " bytes, via " & Compression.GetCompressorName(compressData) & ")."
'If compression failed - or if it resulted in a larger data size - disregard the compression results, and instead
' write out an uncompressed data chunk.
Else
'Debug.Print "Node data showed no benefit from compression; writing uncompressed chunk instead (" & dataPointer & ", " & dataLength & ", " & compressedSize & ")"
SetCompressionFlag nodeIndex, useHeaderBuffer, compressData, False, False
compressedSize = dataLength
'Set the source data pointer to the original pointer, *not* the compressed data
srcDataPointer = dataPointer
End If
'Copy the appropriate data source (compressed or original data, based on the above checks) into our primary
' buffer object, then update the node size flags to match
m_DataBuffer.WriteBytesFromPointer srcDataPointer, compressedSize
If useHeaderBuffer Then
m_NodeDirectory(nodeIndex).NodeHeaderPackedSize = compressedSize
Else
m_NodeDirectory(nodeIndex).NodeDataPackedSize = compressedSize
End If
End If
'This chunk was added successfully! Return TRUE and exit.
AddNodeDataFromPointer = True
End Function
Private Sub SetCompressionFlag(ByVal nodeIndex As Long, ByVal useHeaderBuffer As Boolean, ByVal compressData As PD_CompressionFormat, ByVal newState As Boolean, Optional ByVal updateFileHeaderToo As Boolean = True)
If (compressData = cf_Zstd) Then
If updateFileHeaderToo Then SetBitFlag_Long PDP_HF2_ZstdRequired, newState, m_FileHeader.DataFlags(0)
If useHeaderBuffer Then
SetBitFlag_Long PDP_NF2_ZstdRequiredH, newState, m_NodeDirectory(nodeIndex).NodeFlags(0)
Else
SetBitFlag_Long PDP_NF2_ZstdRequired, newState, m_NodeDirectory(nodeIndex).NodeFlags(0)
End If
ElseIf (compressData = cf_Zlib) Then
If updateFileHeaderToo Then SetBitFlag_Long PDP_HF2_ZlibRequired, newState, m_FileHeader.DataFlags(0)
If useHeaderBuffer Then
SetBitFlag_Long PDP_NF2_ZlibRequiredH, newState, m_NodeDirectory(nodeIndex).NodeFlags(0)
Else
SetBitFlag_Long PDP_NF2_ZlibRequired, newState, m_NodeDirectory(nodeIndex).NodeFlags(0)
End If
ElseIf ((compressData = cf_Lz4) Or (compressData = cf_Lz4hc)) Then
If updateFileHeaderToo Then SetBitFlag_Long PDP_HF2_Lz4Required, newState, m_FileHeader.DataFlags(0)
If useHeaderBuffer Then
SetBitFlag_Long PDP_NF2_Lz4RequiredH, newState, m_NodeDirectory(nodeIndex).NodeFlags(0)
Else
SetBitFlag_Long PDP_NF2_Lz4Required, newState, m_NodeDirectory(nodeIndex).NodeFlags(0)
End If
End If
End Sub
'Thin wrapper to addNodeDataFromPointer, above; if possible, use that function directly to avoid any unnecessary copying of arrays
Friend Function AddNodeDataFromByteArray(ByVal nodeIndex As Long, ByVal useHeaderBuffer As Boolean, ByRef DataBytes() As Byte, Optional ByVal compressData As PD_CompressionFormat = cf_Zstd, Optional ByVal compressionLevel As Long = -1) As Boolean
AddNodeDataFromByteArray = AddNodeDataFromPointer(nodeIndex, useHeaderBuffer, VarPtr(DataBytes(0)), UBound(DataBytes) + 1, compressData, compressionLevel)
End Function
'Thin wrapper for addNodeDataFromPointer, above, but allows the user to supply a string.
Friend Function AddNodeDataFromString(ByVal nodeIndex As Long, ByVal useHeaderBuffer As Boolean, ByRef srcDataString As String, Optional ByVal compressData As PD_CompressionFormat = cf_Zstd, Optional ByVal compressionLevel As Long = -1) As Boolean
If (LenB(srcDataString) <> 0) Then
AddNodeDataFromString = AddNodeDataFromPointer(nodeIndex, useHeaderBuffer, StrPtr(srcDataString), Len(srcDataString) * 2, compressData, compressionLevel)
End If
End Function
'Shortcut function to both create a new node, and copy a file's contents directly into that node, without any intermediate work on the caller's part.
' Note that only the filename and the file's contents are added; things like file attributes or security descriptors are *not*, by design.
'
'At present, default compression settings are used for the added files (e.g. zstd compression is always applied at the default setting).
'
'This function now supports relative paths, e.g. "\Subfolder\Filename.txt". To make use of this feature, you *must* pass the desired
' RELATIVE PATH AND FILENAME as the optional "storeUsingThisRelativePathAndFilename" parameter. (e.g. continuing with the above example,
' you would pass the full absolute path as pathToFile, "C:\User\Subfolder\Filename.txt", then pass "\Subfolder\Filename.txt" as
' storeUsingThisRelativePathAndFilename.) At extraction time, pdPackage will create any required subfolders for you as part of the extraction process.
'
'If you do make use of the relative path feature, note that you *MUST SUPPLY THE FULL RELATIVE PATH* at extraction time, if extracting nodes
' individually. This is required as a pdPackage may contain something like "\Subfolder1\Filename.txt" and "\Subfolder2\Filename.txt", and if
' extracting by filename, it can't differentiate between these two unless you provide the relative path too.
'
'Returns: TRUE if successful.
Friend Function AutoAddNodeFromFile(ByVal pathToFile As String, Optional ByVal OptionalNodeType As Long = 0, Optional ByVal storeUsingThisRelativePathAndFilename As String = vbNullString) As Boolean
'Attempt to load the file into a byte array
Dim fileContents() As Byte
If m_File.FileLoadAsByteArray(pathToFile, fileContents) Then
'Make sure the file loaded successfully
If UBound(fileContents) >= LBound(fileContents) Then
'Create a blank node
Dim nodeIndex As Long
'Files with relative paths in their names are specially flagged, because we need to reconstruct their folder hierarchy
' when extracting them.
Dim filenameIncludesRelativePath As Boolean
filenameIncludesRelativePath = (Len(storeUsingThisRelativePathAndFilename) > 0)
If filenameIncludesRelativePath Then
nodeIndex = AddNode(PDP_DATA_FILEANDPATH, , OptionalNodeType)
Else
nodeIndex = AddNode(PDP_DATA_FILE, , OptionalNodeType)
End If
'Write the filename out to the first node. If the caller specifically specified that the filename includes a relative path,
' we will write the whole filename, untouched
If filenameIncludesRelativePath Then
AutoAddNodeFromFile = AddNodeDataFromString(nodeIndex, True, storeUsingThisRelativePathAndFilename, False)
Else
AutoAddNodeFromFile = AddNodeDataFromString(nodeIndex, True, m_File.FileGetName(pathToFile), False)
End If
'Write the file contents out to the second node
AutoAddNodeFromFile = AutoAddNodeFromFile And AddNodeDataFromByteArray(nodeIndex, False, fileContents)
'Mark a special flag bit in this node, to identify it as an auto-added file entry. This simplifies the extraction process later,
' and also provides a failsafe against the user adding their own nodes called "PDPFILE".
SetBitFlag_Long PDP_HF2_FileCollection, True, m_FileHeader.DataFlags(0)
SetBitFlag_Long PDP_NF2_FileNode, True, m_NodeDirectory(nodeIndex).NodeFlags(0)
End If
Else
'InternalErrorMsg "WARNING! pdPackage.AutoAddNodeFromFile could not load " & pathToFile & ". Abandoning request."
AutoAddNodeFromFile = False
End If
End Function
'Shortcut function to create an arbitrary number of nodes, using some source folder as the reference.
'
'This function is basically a thin wrapper to pdFSO.RetrieveAllFiles and AutoAddNodeFromFile. You could achieve the same results by using
' those two functions yourselves.
'
'The required srcFolder must be an absolute path, including drive letter (e.g. "C:\ThisFolder\SubFolder").
'
'For convenience, every file added via this request can be assigned an optional node type. This same node type can be used when extracting
' files, if you want to add many folder groups to a single pdPackage instance, but still retain the ability to access them individually
' at extraction time.
'
'Subfolder recursion is assumed, but it can be switched off via the optional recurseSubfolders parameter.
'
'When subfolder recursion is in use, each file automatically has a relative path automatically generated for it. By default, this path is
' constructed relative to the srcFolder parameter. If this behavior is not desirable, an alternate base folder can be specified via the
' useCustomRelativeFolderBase string. This is preferable if you are adding multiple folders via this call, but you want all added files
' extracted relative to some parent folder. For example, when updating PD, I manually add the "PhotoDemon\App" subfolder to the update package.
' By default, all added files will be constructed relative to the \App subfolder, so "C:\PhotoDemon\App\Languages\Deutsch.xml" becomes
' "\Languages\Deutsch.xml". At extraction time, this file would be erroneously extracted to "C:\New PD Folder\Languages\Deutsch.xml", because
' the \App folder was the original relative base. To override this behavior, I manually specify a different base folder at creation time,
' e.g. "C:\PhotoDemon\". This causes all added files to receive the relative path "App\Languages\Deutsch.xml", making extraction much simpler.
'
'If you do not want relative paths used whatsoever, set the optional preserveFolders parameter to FALSE. If you do this, note that you cannot
' change your mind at extraction time, as relative paths will not physically exist inside the package.
'
'Default compression settings are assumed for all added files.
'
'Finally, whitelisting or blacklisting behavior is available via the optional onlyAllowTheseExtensions and doNotAllowTheseExtensions parameters.
' These two parameters are passed, untouched, to an underlying pdFSO instance, so please refer to pdFSO for details on how to use these.
'
'Returns: TRUE if all files are added successfully, FALSE otherwise. FALSE should not occur unless read access to target folder(s) is restricted,
' or if memory runs out because the constructed package exceeds available memory.
Friend Function AutoAddNodesFromFolder(ByRef srcFolder As String, Optional ByVal OptionalNodeType As Long = 0, Optional ByVal recurseSubfolders As Boolean = True, Optional ByVal preserveFolders As Boolean = True, Optional ByVal useCustomRelativeFolderBase As String = vbNullString, Optional ByVal onlyAllowTheseExtensions As String = vbNullString, Optional ByVal doNotAllowTheseExtensions As String = vbNullString) As Boolean
'As usual, our lovely pdFSO instance (m_File) is going to handle most the heavy lifting. It will return the list of files to be added
' in a pdStringStack object, which we can then iterate at our leisure.
Dim filesToAdd As pdStringStack
Set filesToAdd = New pdStringStack
'Retrieve all files in the specified folder
If m_File.RetrieveAllFiles(srcFolder, filesToAdd, recurseSubfolders, False, onlyAllowTheseExtensions, doNotAllowTheseExtensions) Then
Dim nodeAdditionSuccess As Boolean
nodeAdditionSuccess = True
'filesToAdd now contains absolute paths to all files we need to add. Pop each file in turn, adding it as we go.
Dim curFile As String, relativeFilePath As String
Do While filesToAdd.PopString(curFile)
'Start by constructing a relative path for this string. (pdFSO could have done this for us automatically, but we need the full
' paths for node addition, and relative paths may vary if the user supplies custom path options.)
'
'First, check for folder preservation.
If preserveFolders Then
'Folder preservation is active. Find the appropriate relative base, per the caller's supplied path(s).
If (LenB(useCustomRelativeFolderBase) <> 0) Then
relativeFilePath = m_File.GenerateRelativePath(useCustomRelativeFolderBase, curFile)
Else
relativeFilePath = m_File.GenerateRelativePath(srcFolder, curFile)
End If
'Folder preservation is inactive, so do not specify any relative paths whatsoever
Else
relativeFilePath = vbNullString
End If
'Add this node
nodeAdditionSuccess = nodeAdditionSuccess And AutoAddNodeFromFile(curFile, OptionalNodeType, relativeFilePath)
Loop
AutoAddNodesFromFolder = nodeAdditionSuccess
'Display some debug info if one or more nodes failed to add
If (Not nodeAdditionSuccess) Then
If recurseSubfolders Then
'InternalErrorMsg "WARNING! AutoAddNodesFromFolder() failed to add one or more nodes. Do you have read access to all subfolders??"
Else
'InternalErrorMsg "WARNING! AutoAddNodesFromFolder() failed to add one or more nodes. Please investigate."
End If
End If
Else
'InternalErrorMsg "WARNING! AutoAddNodesFromFolder() could not retrieve a valid list of files. Do you have read access to the folder??"
AutoAddNodesFromFolder = False
End If
End Function
'Shortcut function to create an arbitrary number of nodes, using a pdStringStack full of files and/or folders as the reference.
' (Obviously, it's up to the caller to populate the string stack.)
'
'This function is basically a thin wrapper to AutoAddNodeFromFile and AutoAddNodesFromFolder. You could achieve identical results by using
' those two functions yourselves.
'
'All files and folders in the in the pdStringStack object must be absolute paths, including drive letter (e.g. "C:\ThisFolder\SubFolder" or
' "C:\ThisFolder\SubFolder\file.txt")
'
'For convenience, every file and/or folder added via this request can be assigned an optional node type. This same node type can be used
' when extracting files, if you want to add multiple file and/or folder groups to a single pdPackage instance, but still retain the ability
' to access them individually at extraction time.
'
'Subfolder recursion is assumed for any folders in the stack, but this behavior can be switched off via the optional recurseSubfolders parameter.
'
'Because the incoming pdStringStack can potentially contain a huge range of possible files and folders, this function is unique in forcing you
' to specify your own custom base folder (relativeFolderBase) against which relative paths will be contructed. If this value is *not* supplied,
' relative paths will not be constructed - e.g. all discovered files will be added WITHOUT folder information attached - so plan accordingly.
'
'Whitelist and blacklist options are disabled for performance reasons. It is assumed that the caller already made use of these, if desired,
' when constructing the incoming string stack.
'
'Default compression settings are assumed for all added files.
'
'Returns: TRUE if all files are added successfully, FALSE otherwise. FALSE should not occur unless read access to target file(s) and/or folder(s)
' is restricted, or if memory runs out because the constructed package exceeds available memory.
Friend Function AutoAddNodesFromStringStack(ByRef srcStringStack As pdStringStack, Optional ByVal relativeFolderBase As String = vbNullString, Optional ByVal OptionalNodeType As Long = 0, Optional ByVal recurseSubfolders As Boolean = True) As Boolean
Dim autoAddSuccess As Boolean
autoAddSuccess = True
'Prior to starting, mark whether relative folders are in use. If they are not, we can bypass some steps within the main Do loop.
Dim relativeFoldersInUse As Boolean
relativeFoldersInUse = (LenB(relativeFolderBase) <> 0)
Dim relativePath As String
'This function relies on autoAddNodesFromFolder and autoAddNodeFromFile to do all the heavy lifting. All we do is pop items off the
' string stack, and forward them to one of those two functions.
Dim curFile As String
Do While srcStringStack.PopString(curFile)
'See if this is a file or a folder
If m_File.FileExists(curFile) Then
'This is a file. Add it via the file function
If relativeFoldersInUse Then
'Construct a relative path
relativePath = m_File.GenerateRelativePath(relativeFolderBase, curFile)
autoAddSuccess = autoAddSuccess And AutoAddNodeFromFile(curFile, OptionalNodeType, relativePath)
Else
autoAddSuccess = autoAddSuccess And AutoAddNodeFromFile(curFile, OptionalNodeType)
End If
ElseIf m_File.PathExists(curFile, False) Then
'This is a folder. Add it via the folder function.
If relativeFoldersInUse Then
autoAddSuccess = autoAddSuccess And AutoAddNodesFromFolder(curFile, OptionalNodeType, recurseSubfolders, True, relativeFolderBase)
Else
autoAddSuccess = autoAddSuccess And AutoAddNodesFromFolder(curFile, OptionalNodeType, recurseSubfolders, False)
End If
Else
'InternalErrorMsg "WARNING! AutoAddNodesFromStringStack() was handed something that isn't a file or folder (" & curFile & ")."
End If
Loop
AutoAddNodesFromStringStack = autoAddSuccess
End Function
'When all nodes have been successfully added, the user can finally write out their data to file. This function will return TRUE
' if successful, FALSE if unsuccessful. (Note that it assumes the caller has done some basic validation on the file path, like
' obtaining permission from the user to overwrite an existing file.)
'
'The only required parameter is the destination filename.
'
'Several optional parameters exist for this function:
' - secondPassDirectoryCompression: compress the directory chunk before writing it out to file.
' - thisIsATempFile: special WAPI flags related to temp files will be set. Note that this flag does not affect file correctness in
' any way; it simply specifies hints related to file caching.
' - compressionLevel: only relevant if secondPassDirectoryCompression is TRUE
' - dstFinalSize: optional [out] parameter to report the final stream size. This is helpful as the actual
' file contents will be written asynchronously, if possible, so attempting to measure
' file size shortly after calling this function brings a high chance of failure.
Friend Function WritePackageToFile(ByVal dstFilename As String, Optional ByVal secondPassDirectoryCompression As PD_CompressionFormat = cf_None, Optional ByVal thisIsATempFile As Boolean = False, Optional ByVal compressionLevel As Long = -1, Optional ByRef dstFinalSize As Long) As Boolean
'If memory is tight, you may choose to free any temporary compression buffers here, as they are no longer needed.
' (In PD, we keep them around as we may reuse a pdPackage instance for multiple writes.)
'Erase m_CompressionBuffer
'Start by updating the file header. Most of this will have been done when the class was initialized, but some values
' can't be set until all nodes have been added.
With m_FileHeader
'Update the final node count
.NodeCount = m_numOfNodes
'Update the size of the directory chunk
.DirectoryChunkSizeOriginal = .NodeStructSize * m_numOfNodes
'We could trim the data buffer here, but it hurts performance and doesn't gain us anything - so instead, just read the
' current stream size and write it into the file header.
.DataChunkSizeOriginal = m_DataBuffer.GetStreamSize
End With
'Before writing the file, we have a few other potential header items we need to construct.
'Create the node directory byte array now. (VB writes UDTs to file in a non-standard, non-obvious way, so we need to do this
' regardless of second-pass directory compression.)
Dim rawDirectoryBuffer() As Byte
Dim originalSize As Long, compressedSize As Long
originalSize = m_FileHeader.DirectoryChunkSizeOriginal
'Use the copyPtrToArray function to copy the contents of m_NodeDirectory into rawDirectoryBuffer(). The function will return TRUE
' if it compresses the data; false, otherwise.
If CopyPtrToArray(VarPtr(m_NodeDirectory(0)), originalSize, rawDirectoryBuffer, compressedSize, secondPassDirectoryCompression, compressionLevel, True) Then
m_FileHeader.DirectoryChunkSizePacked = compressedSize
If ((secondPassDirectoryCompression = cf_Zstd) And (originalSize <> compressedSize)) Then
SetBitFlag_Long PDP_HF2_ZstdRequired, True, m_FileHeader.DirectoryFlags(0)
ElseIf ((secondPassDirectoryCompression = cf_Zlib) And (originalSize <> compressedSize)) Then
SetBitFlag_Long PDP_HF2_ZlibRequired, True, m_FileHeader.DirectoryFlags(0)
ElseIf (((secondPassDirectoryCompression = cf_Lz4) Or (secondPassDirectoryCompression = cf_Lz4hc)) And (originalSize <> compressedSize)) Then
SetBitFlag_Long PDP_HF2_Lz4Required, True, m_FileHeader.DirectoryFlags(0)
End If
Else
m_FileHeader.DirectoryChunkSizePacked = originalSize
End If
'Now, we would typically repeat the above steps for the data chunk. HOWEVER: the data chunk does not current support
' second-pass compression (where the entire data chunk is compressed AGAIN, as-is), so we know its packed size is identical
' to its original size.
m_FileHeader.DataChunkSizePacked = m_FileHeader.DataChunkSizeOriginal
'With all that data known, we can now fill-in the correct offsets inside the file header
m_FileHeader.DataChunkPosition = LenB(m_FileHeader)
m_FileHeader.DirectoryChunkPosition = m_FileHeader.DataChunkPosition + m_FileHeader.DataChunkSizePacked
'If this is a memory-backed stream, kill the destination file if it already exists (because we will be writing the entire
' thing from scratch).
If (m_StreamMode = PD_SM_MemoryBacked) Then
If m_File.FileExists(dstFilename) Then m_File.FileDelete dstFilename
'If this is a file-backed stream, the node data has already been written to file. We just need to add the file header
' and the directory. Close the stream.
Else
dstFinalSize = m_DataBuffer.GetStreamSize()
m_DataBuffer.StopStream
End If
'Retrieve a writable handle
Dim hFile As Long, createSuccess As Boolean
If (m_StreamMode = PD_SM_MemoryBacked) Then
If thisIsATempFile Then
createSuccess = m_File.FileCreateHandle(dstFilename, hFile, True, True, OptimizeTempFile Or OptimizeSequentialAccess)
Else
createSuccess = m_File.FileCreateHandle(dstFilename, hFile, True, True, OptimizeSequentialAccess)
End If
ElseIf (m_StreamMode = PD_SM_FileBacked) Then
createSuccess = m_File.FileCreateAppendHandle(dstFilename, hFile)
End If
If createSuccess Then
Dim mmapHandle As Long, mmapPtr As Long, totalWriteSize As Long, mmapOffset As Long
'Once again, split handling based on the stream type. For a memory-backed stream, we have to write out the entire
' data chunk, but for a file-backed stream, we have much less data to write.
If (m_StreamMode = PD_SM_MemoryBacked) Then
'First, attempt to write out the package using a memory-mapped file. This can be faster (comparable to
' an asynchronous write) vs raw WriteData calls.
totalWriteSize = LenB(m_FileHeader) + m_FileHeader.DataChunkSizePacked + m_FileHeader.DirectoryChunkSizePacked
dstFinalSize = totalWriteSize
Dim useMemMapMode As Boolean
useMemMapMode = m_File.FileConvertHandleToMMPtr(hFile, mmapHandle, mmapPtr, totalWriteSize)
If useMemMapMode Then
'Write out the file data using good ol' RtlMoveMemory!
mmapOffset = LenB(m_FileHeader)
'File header...
CopyMemoryStrict mmapPtr, VarPtr(m_FileHeader), mmapOffset
'Data buffer...
CopyMemoryStrict mmapPtr + mmapOffset, m_DataBuffer.Peek_PointerOnly(0), m_FileHeader.DataChunkSizePacked
'Directory at the end...
CopyMemoryStrict mmapPtr + mmapOffset + m_FileHeader.DataChunkSizePacked, VarPtr(rawDirectoryBuffer(0)), m_FileHeader.DirectoryChunkSizePacked
'Release the mapped view and allow the system to write the file at its leisure
m_File.ReleaseMMHandle mmapHandle, mmapPtr
'If we failed to allocate sufficient memory for a memory-mapped write, fall back to default write techniques
Else
'Writing out the three crucial pieces of data: the header, the data, the directory
With m_File
.FileWriteData hFile, VarPtr(m_FileHeader), LenB(m_FileHeader)
.FileWriteData hFile, m_DataBuffer.Peek_PointerOnly(0), m_FileHeader.DataChunkSizePacked
.FileWriteData hFile, VarPtr(rawDirectoryBuffer(0)), m_FileHeader.DirectoryChunkSizePacked
End With
End If
ElseIf (m_StreamMode = PD_SM_FileBacked) Then
'Write the header at the start of the file
With m_File
.FileMovePointer hFile, 0, FILE_BEGIN
.FileWriteData hFile, VarPtr(m_FileHeader), LenB(m_FileHeader)
'Skip to the end of the file, then write the directory
.FileMovePointer hFile, LenB(m_FileHeader) + m_FileHeader.DataChunkSizePacked, FILE_BEGIN
.FileWriteData hFile, VarPtr(rawDirectoryBuffer(0)), m_FileHeader.DirectoryChunkSizePacked
End With
End If
m_File.FileCloseHandle hFile
WritePackageToFile = True
Else
'InternalErrorMsg "WARNING! WritePackageToFile failed to create a valid destination file handle. Package was not written."
WritePackageToFile = False
End If
End Function
'Copy one array into another, with variable compression. This is used in many places throughout this class; basically any action that
' potentially supports compression should use this function to transfer data between arrays, even when compression is not in use.
' (This function abstracts away all the messy CopyMemoryStrict bits, keeping the rest of the class clean.)
'
'If compression *is* in use, this function will use the requested engine to compress the array accordingly, and it will fill the passed
' dstSize Long with the size (1-based) of the compressed array. Explicit size values are required for both source and destination arrays,
' which allows both the caller and this function to avoid the need for precisely dimensioned arrays. (Any time we can skip an unnecessary
' ReDim Preserve, we save precious processing time.)
'
'This function is designed to be error-proof. If something goes wrong during the compression stage, it will do a straight copy from
' the source to the destination. Similarly, if the compressed size is larger than the uncompressed size, it will also return an
' uncompressed copy of the data. The returned BOOLEAN reports whether or not compression was actually used, NOT whether or not the
' function was successful. (It is assumed the function is always successful, because if compression fails, the default uncompressed
' array will still be returned, and the caller can proceed normally with that data.)
Private Function CopyPtrToArray(ByVal srcPointer As Long, ByVal srcSize As Long, ByRef dstArray() As Byte, ByRef dstSize As Long, Optional ByVal useCompression As PD_CompressionFormat = cf_None, Optional ByVal compressionLevel As Long = -1, Optional ByVal trimDestinationArray As Boolean = False) As Boolean
CopyPtrToArray = Compression.CompressPtrToDstArray(dstArray, dstSize, srcPointer, srcSize, useCompression, compressionLevel, , trimDestinationArray)
End Function
'Close the current package. Any used resources will be immediately freed, so make sure you're finished with the package data
' (e.g. by writing it out to file or something) before closing it!
Friend Sub ClosePackage(Optional ByVal retainInternalBuffers As Boolean = False)
If (Not retainInternalBuffers) Then Erase m_CompressionBuffer
If (Not m_DataBuffer Is Nothing) Then m_DataBuffer.StopStream (m_StreamMode <> PD_SM_MemoryBacked) Or (Not retainInternalBuffers)
End Sub
'Load a pdPackage file into memory.
'
'NOTE: the caller is expected to handle detailed testing of the supplied path (e.g. read access, etc). This function performs
' minimal error checking.
'
'If the file can be successfully loaded and parsed, this function returns TRUE.
'
'Parameters include:
' 1) Source file. (Again, callers must do their own validation on this path.)
' 2) Optionally, a sub-type value you want to validate. If this value is not found in bytes 4-7 of the file header, the file
' will be rejected. (If you didn't request a specific sub-type at pdPackage creation time, don't enable this check!)
' 3) Mode for the pdStream backer. MEMORY mode means the entire file will be immediately read into memory and stored there.
' For small files, this is fine, and will likely result in better node retrieval performance. For large files, however,
' you probably just want the stream to be created in FILE mode. This will only pull data from the file as it is requested,
' and you *must* keep the file alive and accessible for the duration of this pdPackage instance.
' 4) Access rights for the pdStream backer. In READ-ONLY mode, you will not be able to append anything to this pdPackage file.
' However, memory-mapping will be used to load nodes into file, resulting in even better performance. This is the default
' mode, and at present, the only supported mode.
Friend Function ReadPackageFromFile(ByVal srcFilename As String, Optional ByVal subTypeValidator As Long = 0, Optional ByVal streamMode As PD_STREAM_MODE = PD_SM_MemoryBacked, Optional ByVal streamAccess As PD_STREAM_ACCESS = PD_SA_ReadOnly, Optional ByVal optimizeAccess As PD_FILE_ACCESS_OPTIMIZE = OptimizeRandomAccess) As Boolean
On Error GoTo StopPackageFileRead
'If second-pass compression was used (e.g. the entire file was compressed *again* after all nodes were added), we need two levels
' of temporary arrays to handle the decompression process.
Dim tmpCompressionBuffer() As Byte
'Before doing anything else, make sure the file exists
If (Not m_File.FileExists(srcFilename)) Then
'InternalErrorMsg "Requested pdPackage file doesn't exist. Validate all paths before sending them to the pdPackage class!"
GoTo StopPackageFileRead
End If
'Open the file. Note that we play some games with the "optimizeAccess" parameter here. If the caller wants this file
' to be memory-backed, optimizeAccess really doesn't matter (because we're just gonna copy the whole file into memory).
' If, however, the caller wants file-backed mode (where the package is left on-disk, and only accessed as nodes are retrieved),
' then we're gonna silently modify the optimizeAccess to start as Random Access (because we need to jump to the end of the
' file to grab the directory), but after the initial file handle is closed, we'll respect the caller's request inside the
' pdStream object (which grabs the actual nodes).
Dim initialOptimizeAccess As PD_FILE_ACCESS_OPTIMIZE
If (streamMode = PD_SM_MemoryBacked) Then
initialOptimizeAccess = optimizeAccess
ElseIf (streamMode = PD_SM_FileBacked) Then
initialOptimizeAccess = OptimizeRandomAccess
End If
Dim hFile As Long
If m_File.FileCreateHandle(srcFilename, hFile, True, False, initialOptimizeAccess) Then