comparison libinterp/dldfcn/xzip.cc @ 22160:766f934db568

Rewrite gzip and bzip2 in C++ instead of using its applications (bug #43431) * bzip2.m, gzip.m, __xzip__.m: remove old implementation as m files that copy all files into a temporary directory and then call gzip or bzip2 application. Add several new tests and remove duplication of existing tests. * scripts/miscellaneous/module.mk: unlist removed files. * xzip.cc: new implementation of bzip2 and gzip functions making direct use of the libraries in C++. Also add more tests. * libinterp/dldfcn/module-files: list new file and required flags. * configure.ac: add check for bzip2 library.
author Carnë Draug <carandraug@octave.org>
date Sun, 26 Jun 2016 13:32:03 +0200
parents
children 8de49f15e182
comparison
equal deleted inserted replaced
22159:63c806042c27 22160:766f934db568
1 // Copyright (C) 2016 Carnë Draug
2 //
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 //! Octave interface to the compression and uncompression libraries.
17 /*!
18 This was originally implemented as an m file which directly called
19 bzip2 and gzip applications. This may look simpler but causes some
20 issues (see bug #43431) because we have no control over the output
21 file:
22
23 - created file is always in the same directory as the original file;
24 - automatically skip files that already have gz/bz2/etc extension;
25 - some olders versions miss the --keep option.
26
27 In addition, because system() does not have a method that allows
28 passing a list of arguments, there is the issue of having to escape
29 filenames.
30
31 A solution is to pipe file contents into the applications instead of
32 filenames. However, that solution causes:
33
34 # missing file header with original file information;
35 # implementing ourselves the recursive transversion of directories;
36 # do the above in a m file which will be slow;
37 # popen2 is frail on windows.
38
39 */
40
41 #if defined (HAVE_CONFIG_H)
42 # include "config.h"
43 #endif
44
45 #include <cstdio>
46 #include <cstring>
47
48 #include <string>
49 #include <list>
50 #include <functional>
51 #include <stdexcept>
52 #include <iostream>
53 #include <fstream>
54
55 #ifdef HAVE_BZLIB_H
56 # include <bzlib.h>
57 #endif
58
59 #ifdef HAVE_ZLIB_H
60 # include <zlib.h>
61 #endif
62
63 #include "Array.h"
64 #include "str-vec.h"
65 #include "glob-match.h"
66 #include "file-ops.h"
67 #include "dir-ops.h"
68 #include "file-stat.h"
69 #include "oct-env.h"
70
71 #include "defun-dld.h"
72 #include "defun-int.h"
73 #include "errwarn.h"
74
75 class CFile
76 {
77 public:
78 FILE* fp;
79
80 CFile (const std::string& path, const std::string& mode)
81 {
82 fp = std::fopen (path.c_str (), mode.c_str ());
83 if (! fp)
84 throw std::runtime_error ("unable to open file");
85 }
86
87 ~CFile ()
88 {
89 if (std::fclose (fp))
90 // Not pedantic. If this is dest, maybe it failed to flush
91 // so we should signal this before someone removes the source.
92 throw std::runtime_error ("unable to close file");
93 }
94 };
95
96 #ifdef HAVE_BZ2
97 class bz2
98 {
99 private:
100 class zipper
101 {
102 private:
103 int status = BZ_OK;
104 CFile source;
105 CFile dest;
106 BZFILE* bz;
107
108 public:
109 zipper (const std::string& source_path, const std::string& dest_path)
110 : source (source_path, "rb"), dest (dest_path, "wb")
111 {
112 bz = BZ2_bzWriteOpen (&status, dest.fp, 9, 0, 30);
113 if (status != BZ_OK)
114 throw std::runtime_error ("failed to open bzip2 stream");
115 }
116
117 void
118 deflate (void)
119 {
120 const std::size_t buf_len = 8192;
121 char buf[buf_len];
122 std::size_t n_read;
123 while ((n_read = std::fread (buf, sizeof (buf[0]), buf_len, source.fp)) != 0)
124 {
125 if (std::ferror (source.fp))
126 throw std::runtime_error ("failed to read from source file");
127 BZ2_bzWrite (&status, bz, buf, n_read);
128 if (status == BZ_IO_ERROR)
129 throw std::runtime_error ("failed to write or compress");
130 }
131 if (std::ferror (source.fp))
132 throw std::runtime_error ("failed to read from source file");
133 }
134
135 ~zipper ()
136 {
137 int abandon = (status == BZ_IO_ERROR) ? 1 : 0;
138 BZ2_bzWriteClose (&status, bz, abandon, 0, 0);
139 if (status != BZ_OK)
140 throw std::runtime_error ("failed to close bzip2 stream");
141 }
142 };
143
144 public:
145 static const constexpr char* extension = ".bz2";
146
147 static void
148 zip (const std::string& source_path, const std::string& dest_path)
149 {
150 bz2::zipper (source_path, dest_path).deflate ();
151 }
152
153 };
154 #endif // HAVE_BZL2
155
156 // Note about zlib and gzip
157 //
158 // gzip is a format for compressed single files. zlib is a format
159 // designed for in-memory and communication channel applications.
160 // gzip uses the same format internally for the compressed data but
161 // has different headers and trailers.
162 //
163 // zlib is also a library but gzip is not. Very old versions of zlib do
164 // not include functions to create useful gzip headers and trailers:
165 //
166 // Note that you cannot specify special gzip header contents (e.g.
167 // a file name or modification date), nor will inflate tell you what
168 // was in the gzip header. If you need to customize the header or
169 // see what's in it, you can use the raw deflate and inflate
170 // operations and the crc32() function and roll your own gzip
171 // encoding and decoding. Read the gzip RFC 1952 for details of the
172 // header and trailer format.
173 // zlib FAQ
174 //
175 // Recent versions (on which we are already dependent) have deflateInit2()
176 // to do it. We still need to get the right metadata for the header
177 // ourselves though.
178 //
179 // The header is defined in RFC #1952
180 // GZIP file format specification version 4.3
181
182
183 #ifdef HAVE_Z
184 class gz
185 {
186 private:
187
188 // Util class to get a non-const char*
189 class uchar_array
190 {
191 public:
192 // Bytef is a typedef for unsigned char
193 unsigned char* p;
194
195 uchar_array (const std::string& str)
196 {
197 p = new Bytef[str.length () +1];
198 std::strcpy (reinterpret_cast<char*> (p), str.c_str ());
199 }
200
201 ~uchar_array (void) { delete[] p; }
202 };
203
204 // This is the really thing that needs to be
205 class gzip_stream : public z_stream
206 {
207 public:
208 gzip_stream ()
209 {
210 zalloc = Z_NULL;
211 zfree = Z_NULL;
212 opaque = Z_NULL;
213 }
214
215 ~gzip_stream ()
216 {
217 int status = deflateEnd (this);
218 if (status != Z_OK)
219 throw std::runtime_error ("failed to close zlib stream");
220 }
221 };
222
223 class gzip_header : public gz_header
224 {
225 private:
226 uchar_array basename;
227
228 public:
229 gzip_header (const std::string& source_path)
230 : basename (octave::sys::env::base_pathname (source_path))
231 {
232 const octave::sys::file_stat source_stat (source_path);
233 if (! source_stat)
234 throw std::runtime_error ("unable to stat source file");
235
236 // time_t may be a signed int in which case it will be a
237 // positive number so it is safe to uLong. Or is it? Can
238 // unix_time really never be negative?
239 time = uLong (source_stat.mtime ().unix_time ());
240
241 // If FNAME is set, an original file name is present,
242 // terminated by a zero byte. The name must consist of ISO
243 // 8859-1 (LATIN-1) characters; on operating systems using
244 // EBCDIC or any other character set for file names, the name
245 // must be translated to the ISO LATIN-1 character set. This
246 // is the original name of the file being compressed, with any
247 // directory components removed, and, if the file being
248 // compressed is on a file system with case insensitive names,
249 // forced to lower case.
250 name = basename.p;
251
252 // If we don't set it to Z_NULL, then it will set FCOMMENT (4th bit)
253 // on the FLG byte, and then write {0, 3} comment.
254 comment = Z_NULL;
255
256 // Seems to already be the default but we are not taking chances.
257 extra = Z_NULL;
258
259 // We do not want a CRC for the header. That would be only 2 more
260 // bytes, and maybe it would be a good thing but we want to generate
261 // gz files similar to the default gzip application.
262 hcrc = 0;
263
264 // OS (Operating System):
265 // 0 - FAT filesystem (MS-DOS, OS/2, NT/Win32)
266 // 1 - Amiga
267 // 2 - VMS (or OpenVMS)
268 // 3 - Unix
269 // 4 - VM/CMS
270 // 5 - Atari TOS
271 // 6 - HPFS filesystem (OS/2, NT)
272 // 7 - Macintosh
273 // 8 - Z-System
274 // 9 - CP/M
275 // 10 - TOPS-20
276 // 11 - NTFS filesystem (NT)
277 // 12 - QDOS
278 // 13 - Acorn RISCOS
279 // 255 - unknown
280 //
281 // The list is problematic because it mixes OS and filesystem. It
282 // also does not specify whether filesystem relates to source or
283 // destination file.
284
285 #if defined (__WIN32__)
286 os = 0; // or should it be 11?
287 #elif defined (__APPLE__)
288 os = 7;
289 #else // unix by default?
290 os = 3;
291 #endif
292 }
293 };
294
295 class zipper
296 {
297 private:
298 CFile source;
299 CFile dest;
300 gzip_header header;
301 gzip_stream strm = gzip_stream ();
302
303 public:
304 zipper (const std::string& source_path, const std::string& dest_path)
305 : source (source_path, "rb"), dest (dest_path, "wb"),
306 header (source_path)
307 { }
308
309 void
310 deflate ()
311 {
312 // int deflateInit2 (z_streamp strm,
313 // int level, // compression level (default is 8)
314 // int method,
315 // int windowBits, // 15 (default) + 16 (gzip format)
316 // int memLevel, // memory usage (default is 8)
317 // int strategy);
318
319 int status = deflateInit2 (&strm, 8, Z_DEFLATED, 31, 8,
320 Z_DEFAULT_STRATEGY);
321 if (status != Z_OK)
322 throw std::runtime_error ("failed to open zlib stream");
323
324 deflateSetHeader (&strm, &header);
325
326 const std::size_t buf_len = 8192;
327 unsigned char buf_in[buf_len];
328 unsigned char buf_out[buf_len];
329
330 while ((strm.avail_in = std::fread (buf_in, sizeof (buf_in[0]),
331 buf_len, source.fp)) != 0)
332 {
333 if (std::ferror (source.fp))
334 throw std::runtime_error ("failed to read source file");
335
336 strm.next_in = buf_in;
337 const int flush = std::feof (source.fp) ? Z_FINISH : Z_NO_FLUSH;
338
339 // If deflate returns Z_OK and with zero avail_out, it must be
340 // called again after making room in the output buffer because
341 // there might be more output pending.
342 do
343 {
344 strm.avail_out = buf_len;
345 strm.next_out = buf_out;
346 status = ::deflate (&strm, flush);
347 if (status == Z_STREAM_ERROR)
348 throw std::runtime_error ("failed to deflate");
349
350 std::fwrite (buf_out, sizeof (buf_out[0]),
351 buf_len - strm.avail_out, dest.fp);
352 if (std::ferror (dest.fp))
353 throw std::runtime_error ("failed to write file");
354 }
355 while (strm.avail_out == 0);
356
357 if (strm.avail_in != 0)
358 throw std::runtime_error ("failed to wrote file");
359 }
360 }
361 };
362
363 public:
364 static const constexpr char* extension = ".gz";
365
366 static void
367 zip (const std::string& source_path, const std::string& dest_path)
368 {
369 gz::zipper (source_path, dest_path).deflate ();
370 }
371 };
372 #endif // HAVE_Z
373
374
375 template<typename X>
376 string_vector
377 xzip (const Array<std::string>& source_patterns,
378 const std::function<std::string(const std::string&)>& mk_dest_path)
379 {
380 std::list<std::string> dest_paths;
381
382 std::function<void(const std::string&)> walk;
383 walk = [&walk, &mk_dest_path, &dest_paths] (const std::string& path) -> void
384 {
385 const octave::sys::file_stat fs (path);
386 // is_dir and is_reg will return false if failed to stat.
387 if (fs.is_dir ())
388 {
389 octave::sys::dir_entry dir (path);
390 if (dir)
391 {
392 // Collect the whole list of filenames first, before recursion
393 // to avoid issues with infinite loop if the action generates
394 // files in the same directory (highly likely).
395 string_vector dirlist = dir.read ();
396 for (octave_idx_type i = 0; i < dirlist.numel (); i++)
397 if (dirlist(i) != "." && dirlist(i) != "..")
398 walk (octave::sys::file_ops::concat (path, dirlist(i)));
399 }
400 // Note that we skip any problem with directories.
401 }
402 else if (fs.is_reg ())
403 {
404 const std::string dest_path = mk_dest_path (path);
405 try
406 {
407 X::zip (path, dest_path);
408 }
409 catch (...)
410 {
411 // Error "handling" is not including filename on the output list.
412 // Also remove created file which maybe was not even created
413 // in the first place. Note that it is possible for the file
414 // to exist in the first place and for for X::zip to not have
415 // clobber it yet but we remove it anyway by design.
416 octave::sys::unlink (dest_path);
417 return;
418 }
419 dest_paths.push_front (dest_path);
420 }
421 // Skip all other file types and errors.
422 return;
423 };
424
425 for (octave_idx_type i = 0; i < source_patterns.numel (); i++)
426 {
427 const glob_match pattern (octave::sys::file_ops::tilde_expand (source_patterns(i)));
428 const string_vector filepaths = pattern.glob ();
429 for (octave_idx_type j = 0; j < filepaths.numel (); j++)
430 walk (filepaths(j));
431 }
432 return string_vector (dest_paths);
433 }
434
435
436 template<typename X>
437 string_vector
438 xzip (const Array<std::string>& source_patterns)
439 {
440 const std::string ext = X::extension;
441 const std::function<std::string(const std::string&)> mk_dest_path
442 = [&ext] (const std::string& source_path) -> std::string
443 {
444 return source_path + ext;
445 };
446 return xzip<X> (source_patterns, mk_dest_path);
447 }
448
449 template<typename X>
450 string_vector
451 xzip (const Array<std::string>& source_patterns, const std::string& out_dir)
452 {
453 const std::string ext = X::extension;
454 const std::function<std::string(const std::string&)> mk_dest_path
455 = [&out_dir, &ext] (const std::string& source_path) -> std::string
456 {
457 const std::string basename = octave::sys::env::base_pathname (source_path);
458 return octave::sys::file_ops::concat (out_dir, basename + ext);
459 };
460
461 // We don't care if mkdir fails. Maybe it failed because it already
462 // exists, or maybe it can't bre created. If the first, then there's
463 // nothing to do, if the later, then it will be handled later. Any
464 // is to be handled by not listing files in the output.
465 octave::sys::mkdir (out_dir, 0777);
466 return xzip<X> (source_patterns, mk_dest_path);
467 }
468
469 template<typename X>
470 static octave_value_list
471 xzip (const std::string& func_name, const octave_value_list& args)
472 {
473 const octave_idx_type nargin = args.length ();
474 if (nargin < 1 || nargin > 2)
475 print_usage ();
476
477 const Array<std::string> source_patterns
478 = args(0).xcellstr_value ("%s: FILES must be a character array or cellstr",
479 func_name.c_str ());
480 if (nargin == 1)
481 return octave_value (Cell (xzip<X> (source_patterns)));
482 else // nargin == 2
483 {
484 const std::string out_dir = args(1).string_value ();
485 return octave_value (Cell (xzip<X> (source_patterns, out_dir)));
486 }
487 }
488
489 DEFUN_DLD (gzip, args, ,
490 doc: /* -*- texinfo -*-
491 @deftypefn {} {@var{filelist} =} gzip (@var{files})
492 @deftypefnx {} {@var{filelist} =} gzip (@var{files}, @var{dir})
493 Compress the list of files and directories specified in @var{files}.
494
495 @var{files} is a character array or cell array of strings. Shell wildcards
496 in the filename such as @samp{*} or @samp{?} are accepted and expanded.
497 Each file is compressed separately and a new file with a @file{".gz"}
498 extension is created. The original files are not modified, but existing
499 compressed files will be silently overwritten. If a directory is
500 specified then @code{gzip} recursively compresses all files in the
501 directory.
502
503 If @var{dir} is defined the compressed files are placed in this directory,
504 rather than the original directory where the uncompressed file resides.
505 Note that this does not replicate a directory tree in @var{dir} which may
506 lead to files overwritting each other if there are multiple files with the
507 same name.
508
509 If @var{dir} does not exist it is created.
510
511 The optional output @var{filelist} is a list of the compressed files.
512 @seealso{gunzip, unpack, bzip2, zip, tar}
513 @end deftypefn */)
514 {
515 #ifndef HAVE_Z
516 err_disabled_feature ("gzip", "gzip");
517 #else
518 return xzip<gz> ("gzip", args);
519 #endif
520 }
521
522 /*
523 %!error gzip ()
524 %!error gzip ("1", "2", "3")
525 %!error <FILES must be a character array or cellstr> gzip (1)
526 */
527
528 DEFUN_DLD (bzip2, args, ,
529 doc: /* -*- texinfo -*-
530 @deftypefn {} {@var{filelist} =} bzip2 (@var{files})
531 @deftypefnx {} {@var{filelist} =} bzip2 (@var{files}, @var{dir})
532 Compress the list of files specified in @var{files}.
533
534 @var{files} is a character array or cell array of strings. Shell wildcards
535 in the filename such as @samp{*} or @samp{?} are accepted and expanded.
536 Each file is compressed separately and a new file with a @file{".bz2"}
537 extension is created. The original files are not modified, but existing
538 compressed files will be silently overwritten.
539
540 If @var{dir} is defined the compressed files are placed in this directory,
541 rather than the original directory where the uncompressed file resides.
542 Note that this does not replicate a directory tree in @var{dir} which may
543 lead to files overwritting each other if there are multiple files with the
544 same name.
545
546 If @var{dir} does not exist it is created.
547
548 The optional output @var{filelist} is a list of the compressed files.
549 @seealso{bunzip2, unpack, gzip, zip, tar}
550 @end deftypefn */)
551 {
552 #ifndef HAVE_BZ2
553 err_disabled_feature ("bzip2", "bzip2");
554 #else
555 return xzip<bz2> ("bzip2", args);
556 #endif
557 }
558
559 // Tests for both gzip/bzip2 and gunzip/bunzip2
560 /*
561
562 ## Takes a single argument, a function handle for the test. This other
563 ## function must accept two arguments, a directory for the tests, and
564 ## a cell array with zip function, unzip function, and file extension.
565
566 %!function run_test_function (test_function)
567 %! enabled_zippers = cell (0, 0);
568 %! if (__octave_config_info__ ().build_features.BZ2)
569 %! enabled_zippers(1, end+1) = @bzip2;
570 %! enabled_zippers(2, end) = @bunzip2;
571 %! enabled_zippers(3, end) = ".bz2";
572 %! endif
573 %! if (__octave_config_info__ ().build_features.Z)
574 %! enabled_zippers(1, end+1) = @gzip;
575 %! enabled_zippers(2, end) = @gunzip;
576 %! enabled_zippers(3, end) = ".gz";
577 %! endif
578 %!
579 %! for z = enabled_zippers
580 %! test_dir = tempname ();
581 %! if (! mkdir (test_dir))
582 %! error ("unable to create directory for tests");
583 %! endif
584 %! unwind_protect
585 %! test_function (test_dir, z)
586 %! unwind_protect_cleanup
587 %! confirm_recursive_rmdir (false, "local");
588 %! rmdir (test_dir, "s");
589 %! end_unwind_protect
590 %! endfor
591 %!endfunction
592
593 %!function create_file (fpath, data)
594 %! fid = fopen (fpath, "wb");
595 %! if (fid < 0)
596 %! error ("unable to open file for writing");
597 %! endif
598 %! if (fwrite (fid, data, class (data)) != numel (data))
599 %! error ("unable to write to file");
600 %! endif
601 %! if (fflush (fid) || fclose (fid))
602 %! error ("unable to flush or close file");
603 %! endif
604 %!endfunction
605
606 ## Test with large files because of varied buffer size
607 %!function test_large_file (test_dir, z)
608 %! test_file = tempname (test_dir);
609 %! create_file (test_file, rand (500000, 1));
610 %! md5 = hash ("md5", fileread (test_file));
611 %!
612 %! expected_z_file = [test_file z{3}];
613 %! z_files = z{1} (test_file);
614 %! assert (z_files, {expected_z_file})
615 %!
616 %! unlink (test_file);
617 %! assert (z{2} (z_files{1}), {test_file})
618 %! assert (hash ("md5", fileread (test_file)), md5)
619 %!endfunction
620 %!test run_test_function (@test_large_file)
621
622 ## Test that xzipped files are rexzipped
623 %!function test_z_z (test_dir, z)
624 %! ori_file = tempname (test_dir);
625 %! create_file (ori_file, rand (100, 1));
626 %! md5_ori = hash ("md5", fileread (ori_file));
627 %!
628 %! z_file = [ori_file z{3}];
629 %! filelist = z{1} (ori_file);
630 %! assert (filelist, {z_file}) # check output
631 %! assert (exist (z_file), 2) # confirm file exists
632 %! assert (exist (z_file), 2) # and did not remove original file
633 %! md5_z = hash ("md5", fileread (z_file));
634 %!
635 %! unlink (ori_file);
636 %! assert (z{2} (z_file), {ori_file})
637 %! ## bug #48597
638 %! assert (exist (ori_file), 2) # bug #48597 (Xunzip should not remove file)
639 %! assert (hash ("md5", fileread (ori_file)), md5_ori)
640 %!
641 %! ## xzip should dutifully re-xzip files even if they already are zipped
642 %! z_z_file = [z_file z{3}];
643 %!
644 %! filelist = z{1} (z_file);
645 %! assert (filelist, {z_z_file}) # check output
646 %! assert (exist (z_z_file), 2) # confirm file exists
647 %! assert (exist (z_z_file), 2) # and did not remove original file
648 %!
649 %! unlink (z_file);
650 %! assert (z{2} (z_z_file), {z_file})
651 %! assert (hash ("md5", fileread (z_file)), md5_z)
652 %!endfunction
653 %!test run_test_function (@test_z_z)
654
655 %!function test_xzip_dir (test_dir, z)
656 %! fpaths = fullfile (test_dir, {"test1", "test2", "test3"});
657 %! z_files = strcat (fpaths, z{3});
658 %! md5s = cell (1, 3);
659 %! for idx = 1:numel(fpaths)
660 %! create_file (fpaths{idx}, rand (100, 1));
661 %! md5s(idx) = hash ("md5", fileread (fpaths{idx}));
662 %! endfor
663 %!
664 %! assert (sort (z{1} ([test_dir filesep()])), z_files(:))
665 %! for idx = 1:numel(fpaths)
666 %! assert (exist (z_files{idx}), 2)
667 %! unlink (fpaths{idx});
668 %! endfor
669 %! for idx = 1:numel(fpaths)
670 %! assert (z{2} (z_files{idx}), fpaths{idx}); # bug #48598
671 %! assert (hash ("md5", fileread (fpaths{idx})), md5s{idx})
672 %! endfor
673 %!endfunction
674 %!test run_test_function (@test_xzip_dir)
675
676 %!function test_save_to_dir (test_dir, z)
677 %! filename = "test-file";
678 %! filepath = fullfile (test_dir, filename);
679 %! create_file (filepath, rand (100, 1));
680 %! md5 = hash ("md5", fileread (filepath));
681 %!
682 %! ## test with existing and non-existing directory
683 %! out_dirs = {tempname (test_dir), tempname (test_dir)};
684 %! if (! mkdir (out_dirs{1}))
685 %! error ("unable to create directory for test");
686 %! endif
687 %! for idx = 1:numel(out_dirs)
688 %! out_dir = out_dirs{idx};
689 %! z_file = fullfile (out_dir, [filename z{3}]);
690 %! assert (z{1} (filepath, out_dir), {z_file})
691 %! assert (exist (z_file, "file"), 2)
692 %! uz_file = z_file(1:(end-numel(z{3})));
693 %! assert (z{2} (z_file), uz_file); # bug #48598
694 %! assert (hash ("md5", fileread (uz_file)), md5)
695 %! endfor
696 %!endfunction
697 %!test run_test_function (@test_save_to_dir)
698 */