[ANNOUNCE] libguestfs 1.4.0 - tools for accessing and modifying disk images and virtual machines
by Richard W.M. Jones
I'm pleased to announce a major new release of libguestfs.
libguestfs is a library and a set of tools for accessing and modifying
disk images and virtual machines. You can use this for viewing and
editing files inside guests, scripting changes to VMs, monitoring disk
used/free statistics, P2V, V2V, performing partial backups, cloning
VMs, and much more.
Home page: http://libguestfs.org/
Downloads: http://libguestfs.org/download/?C=M;O=D
Release notes for libguestfs 1.4.0
----------------------------------
These release notes only cover the differences from the previous
stable/dev branch split (1.2.0). For detailed changelogs, please see
the git repository, or the ChangeLog file distributed in the tarball.
New features
- guestfish lets you choose a prepared disk image, eg:
guestfish -N fs:ext4
- Add write support to guestmount (FUSE) module.
- virt-resize can now resize the content of partitions and logical
volumes in the guest, and we have better support for shrinking guests.
- Bash tab-completion script for guestfish.
- Add ZFS support to virt-rescue.
- New tool 'virt-make-fs' for creating filesystems with content.
- Allow suffixes on any guestfish number parameter, eg. "1M".
- guestfish 'man' command opens the manual page.
- guestfish supports a "heredoc" syntax for uploading files:
upload -<<_end_ /foo
content
_end_
- Some guestfish commands now print their output in octal or hex
where appropriate (RHBZ#583242).
- Allow dash prefix on guestfish command line. This ignores any
error from the second command: (RHBZ#578407)
guestfish -- cmd1 : -cmd2 : cmd3
- guestfish -h / help command now returns an error for non-existent
commands (RHBZ#597145).
- New 'supported' command in guestfish to list optional groups of
commands which are supported by the daemon / configuration.
- virt-inspector and guestfish -i now work for filenames which
contain spaces (RHBZ#507810).
- Change the protocol to use link-local addresses, to avoid
conflicting with any address that the host might be using
(RHBZ#588763).
- libguestfs now sets the correct time and timezone on filesystem
modifications.
- Sort the domains into alphabetical order in virt-df.
- Make mkfs-b command work for FAT and NTFS by mapping the blocksize
parameter to the cluster size (RHBZ#599464).
- Add version numbers to Perl modules (RHBZ#521674).
- Localization now works for all the libguestfs tools (RHBZ#559963).
- Tools now support filesystem-on-image VMs (RHBZ#590167).
- virt-list-partitions has a '-t' option to show the total size of disks.
- Include extra Augeas lenses in the supermin appliance (Matthew Booth).
- Add error and close callbacks.
- Add explicit close method in the Perl API.
- Multiple fixes for RHEL 5 compatibility.
- Multiple fixes for Debian/Ubuntu compatibility.
- Multiple revisions to improve the documentation.
Security
- Fix a potential DoS in virt-inspector and virt-v2v if a specially
crafted disk image contained a char device in place of one of the
configuration files that we read under /etc (RHBZ#582484).
New APIs
- aug-clear - clear Augeas path
- available-all-groups - return a list of all optional groups
- base64-in - upload base64-encoded data to file
- base64-out - download file and encode as base64
- checksum-device - compute checksums on the contents of a device
- checksums-out - compute checksums of multiple files in a directory
- debug-upload - upload a file to the appliance
- fallocate64 - preallocate a file in the guest filesystem
- fill-pattern - fill a file with a repeating pattern of bytes
- get-umask - get the current umask
- lvresize-free - expand an LV to fill free space
- ntfsresize - resize an NTFS filesystem
- ntfsresize-size - resize an NTFS filesystem (with size)
- part-del - delete a partition
- part-get-bootable - get the bootable flag of a partition
- part-get-mbr-id - get the MBR type byte of a partition
- part-set-mbr-id - set the MBR type byte of a partition
- pvresize-size - resize a physical volume (with size)
- pwrite - write to part of a file
- resize2fs-size - resize an ext2/3/4 filesystem (with size)
- txz-in - unpack compressed tarball to directory (RHBZ#580556)
- txz-out - pack directory into compressed tarball (RHBZ#580556)
- vfs-label - get the filesystem label
- vfs-uuid - get the filesystem UUID
- vgscan - rescan for LVM physical volumes, volume groups and logical volumes
- write - create a new file
- zero-device - write zeroes to an entire device
Internals
- Extend the generator to support testing optional features.
- Stricter checks on input parameters to many calls (RHBZ#501893 RHBZ#501894)
- Extend the protocol to support sending arbitrary 8 bit data buffers.
- Ship 'BUGS' file with releases. This is a summary of the bugs in
the Red Hat Bugzilla database.
- Ship 'RELEASE-NOTES' file with releases, containing release notes.
- Unify supermin appliance building into one place, in febootstrap 2.7.
- Fix the protocol code to handle the case where both ends send cancel
messages at the same time.
Bugs fixed
- 612178 guestfish: using -m option in conjunction with --listen option causes appliance to die
- 610880 libguestfs should set broader read perms on tmpdir, so works in some situations when executed with umask 077
- 604691 OCaml bindings are not thread safe
- 603870 Updates to Spanish translation
- 602592 [RFE] expose guestfs_close in perl bindings
- 600977 virt-df -h --csv "Argument .. isn't numeric in printf"
- 599464 mkfs-b does not support vfat/ntfs
- 598807 add_cdrom does not work in RHEL 6
- 598309 part-list and several other cmd failed on libguestfs on RHEL5
- 597145 guestfish 'help' command should indicate error in exit status with an unknown command
- 597135 guestfish write-file cmd does not check "size" parameter
- 597118 A warning should be given in the help of mke2journal-L for the length of label
- 597112 get-e2uuid should use blkid instead of "tune2fs -l" to get filesystem UUID
- 596776 virt-inspector doesn't discover modprobe aliases on RHEL 3 guests
- 596763 Updates to Spanish translation
- 593292 Updates to Spanish translation
- 592883 can not edit files on images mounted with guestmount cmd
- 592360 Updates to Spanish translation
- 591250 virt-tar prints "tar_in: tar subcommand failed on directory" if the archive is compressed or not in the right format
- 591155 virt-tar prints "tar_in: tar subcommand failed on directory" if a disk image is not writable
- 591142 virt-inspector should give an error for unrecognized disk images
- 590167 virt-inspector and other virt tools should be able to handle filesystem-on-image VMs
- 589039 guestfish read-file cmd will cause daemon hang when read large files
- 588851 guestfs_launch() returns -1, but guestfs_last_error() == NULL
- 588763 libguestfs should use non-public or link-local addresses for appliance network
- 588733 Updates to Spanish translation
- 588651 guestfish 'strings-e' cmd does not give proper error message or hint
- 587484 lvresize can't reduce size of a volumn
- 585961 Updates to Spanish translation
- 585223 ntfsresize should support shrinking filesystems
- 585222 pvresize should support shrinking PVs
- 585221 resize2fs should support shrinking filesystems
- 584038 Updates to Spanish translation
- 583554 [FEAT] mknod-mode command is needed to set mode explicitly
- 583242 [RFE] guestfish should print outputs in a suitable base (eg. octal for modes)
- 582993 guestfish eats words when tab completing case (in)sensitive paths
- 582953 Misleading help information about lvcreate command
- 582948 mknod command doesn't make block, character or FIFO devices
- 582929 mknod doesn't check for invalid mode
- 582901 guestfish chmod/umask commands do not check invalid mode value
- 582899 guestfish:sparse is missed from command autocomplete list
- 582891 [Feature Request] behavior and return value of guestfish umask cmd should be changed
- 582548 [mknod] umask shouldn't take effect when mode is set explicitly
- 582484 some guestfish sub commands can not handle special files properly
- 582252 Updates to Spanish translation
- 581501 Updates to Spanish translation
- 580650 virt-inspector warns "No grub default specified at /usr/lib/perl5/Sys/Guestfs/Lib.pm at [...]"
- 580556 request for libguestfs to support .txz tarballs
- 580246 tar-in command hangs if uploading more than available space
- 580016 aug-ls in guestfish does not take augeas variable as argument
- 579664 guestfish doesn't report error when there is not enough space for image allocation
- 579608 multiple commands in guestfish can not work for symbol links
- 579155 libguestfs hangs if qemu doesn't start (in null vmchannel mode)
- 578407 the prefix '-' in sub-command isn't handled by guestfish in remote control mode
- 576879 libguestfs protocol loses synchronization if you 'upload' before mounting disks
- 559963 libguestfs Perl programs do set locale, but still localization doesn't work
- 521674 Perl modules are unversioned, but should carry version numbers
- 516096 Race condition in test_swapon_label_0: /sbin/blockdev: BLKRRPART: Device or resource busy
- 507810 guestfish -i / virt-inspector cannot handle spaces in filenames
- 502533 Updated Polish translation of libguestfs
- 501894 Some String parameters should be OptString
- 501893 String parameters should be checked for != NULL
- 501889 write-file does not support strings containing ASCII NUL
- 484986 grub-install fails on virtio disk
Release notes for previous versions of libguestfs
-------------------------------------------------
2009-11-10 : 1.0.78
https://www.redhat.com/archives/libguestfs/2009-November/msg00095.html
2009-09-13 : 1.0.67
https://www.redhat.com/archives/libguestfs/2009-August/msg00281.html
2009-07-23 : 1.0.64
https://www.redhat.com/archives/libguestfs/2009-July/msg00059.html
2009-07-14 : 1.0.59
https://www.redhat.com/archives/libguestfs/2009-July/msg00023.html
--
Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones
Read my programming blog: http://rwmj.wordpress.com
Fedora now supports 80 OCaml packages (the OPEN alternative to F#)
http://cocan.org/getting_started_with_ocaml_on_red_hat_and_fedora
14 years, 3 months
[PATCH] hivex: add hivex_set_value api call and ocaml/perl bindings, tests
by Conrad Meyer
Round 3 -- this time with working OCaml bindings.
(I'm not on the list, please copy me on replies, thanks.)
---
generator/generator.ml | 77 ++++++++++++++++++++++++++++++++++++++++-
lib/hivex.c | 90 ++++++++++++++++++++++++++++++++++++++++++++++++
perl/t/201-setvalue.t | 54 ++++++++++++++++++++++++++++
3 files changed, 219 insertions(+), 2 deletions(-)
create mode 100644 perl/t/201-setvalue.t
diff --git a/generator/generator.ml b/generator/generator.ml
index 5a0ab6e..2b915fd 100755
--- a/generator/generator.ml
+++ b/generator/generator.ml
@@ -71,6 +71,7 @@ and argt = (* Note, cannot be NULL/0 unless it
| AOpenFlags (* HIVEX_OPEN_* flags list. *)
| AUnusedFlags (* Flags arg that is always 0 *)
| ASetValues (* See hivex_node_set_values. *)
+ | ASetValue (* See hivex_node_set_value. *)
(* Hive types, from:
* https://secure.wikimedia.org/wikipedia/en/wiki/Windows_Registry#Keys_and_...
@@ -304,8 +305,15 @@ subnodes become invalid. You cannot delete the root node.";
"set (key, value) pairs at a node",
"\
This call can be used to set all the (key, value) pairs
-stored in C<node>. Note that this library does not offer
-a way to modify just a single key at a node.
+stored in C<node>.
+
+C<node> is the node to modify.";
+
+ "node_set_value", (RErr, [AHive; ANode "node"; ASetValue; AUnusedFlags]),
+ "set a single (key, value) pair at a given node",
+ "\
+This call can be used to set a single (key, value) pair
+stored in C<node>.
C<node> is the node to modify.";
]
@@ -459,6 +467,7 @@ let name_of_argt = function
| ANode n | AValue n | AString n | AStringNullable n -> n
| AOpenFlags | AUnusedFlags -> "flags"
| ASetValues -> "values"
+ | ASetValue -> "val"
(* Check function names etc. for consistency. *)
let check_functions () =
@@ -806,6 +815,7 @@ and generate_c_prototype ?(extern = false) name style =
| AString n | AStringNullable n -> pr "const char *%s" n
| AOpenFlags | AUnusedFlags -> pr "int flags"
| ASetValues -> pr "size_t nr_values, const hive_set_value *values"
+ | ASetValue -> pr "const hive_set_value *val"
) (snd style);
(match fst style with
| RLenType | RLenTypeVal -> pr ", hive_type *t, size_t *len"
@@ -937,6 +947,11 @@ Any existing values stored at the node are discarded, and their
C<hive_value_h> handles become invalid. Thus you can remove all
values stored at C<node> by passing C<nr_values = 0>.\n\n";
+ if List.mem ASetValue (snd style) then
+ pr "C<value> is a single (key, value) pair.
+
+Existing C<hive_value_h> handles become invalid.\n\n";
+
(match fst style with
| RErr ->
pr "\
@@ -1478,6 +1493,7 @@ and generate_ocaml_prototype ?(is_external = false) name style =
| AOpenFlags -> pr "open_flag list -> "
| AUnusedFlags -> ()
| ASetValues -> pr "set_value array -> "
+ | ASetValue -> pr "set_value -> "
) (snd style);
(match fst style with
| RErr -> pr "unit" (* all errors are turned into exceptions *)
@@ -1548,6 +1564,7 @@ caml_raise_with_args (value tag, int nargs, value args[])
#define Hiveh_val(v) (*((hive_h **)Data_custom_val(v)))
static value Val_hiveh (hive_h *);
static int HiveOpenFlags_val (value);
+static hive_set_value *HiveSetValue_val (value);
static hive_set_value *HiveSetValues_val (value);
static hive_type HiveType_val (value);
static value Val_hive_type (hive_type);
@@ -1621,6 +1638,8 @@ static void raise_closed (const char *) Noreturn;
| ASetValues ->
pr " int nrvalues = Wosize_val (valuesv);\n";
pr " hive_set_value *values = HiveSetValues_val (valuesv);\n"
+ | ASetValue ->
+ pr " hive_set_value *val = HiveSetValue_val (valv);\n"
) (snd style);
pr "\n";
@@ -1688,6 +1707,9 @@ static void raise_closed (const char *) Noreturn;
| ASetValues ->
pr " free (values);\n";
pr "\n";
+ | ASetValue ->
+ pr " free (val);\n";
+ pr "\n";
) (snd style);
(* Check for errors. *)
@@ -1750,6 +1772,19 @@ HiveOpenFlags_val (value v)
}
static hive_set_value *
+HiveSetValue_val (value v)
+{
+ hive_set_value *val = malloc (sizeof (hive_set_value));
+
+ val->key = String_val (Field (v, 0));
+ val->t = HiveType_val (Field (v, 1));
+ val->len = caml_string_length (Field (v, 2));
+ val->value = String_val (Field (v, 2));
+
+ return val;
+}
+
+static hive_set_value *
HiveSetValues_val (value v)
{
size_t nr_values = Wosize_val (v);
@@ -2113,6 +2148,7 @@ and generate_perl_prototype name style =
| AOpenFlags -> pr "[flags]"
| AUnusedFlags -> assert false
| ASetValues -> pr "\\@values"
+ | ASetValue -> pr "$val"
) args;
pr ")"
@@ -2243,6 +2279,39 @@ unpack_pl_set_values (SV *sv)
return ret;
}
+static hive_set_value *
+unpack_set_value (SV *sv)
+{
+ hive_set_value *ret;
+
+ if (!sv || !SvROK (sv) || SvTYPE (SvRV (sv)) != SVt_PVHV)
+ croak (\"not a hash ref\");
+
+ ret = malloc (sizeof (hive_set_value));
+ if (ret == NULL)
+ croak (\"malloc failed\");
+
+ HV *hv = (HV *)SvRV(sv);
+
+ SV **svp;
+ svp = hv_fetch (hv, \"key\", 3, 0);
+ if (!svp || !*svp)
+ croak (\"missing 'key' in hash\");
+ ret->key = SvPV_nolen (*svp);
+
+ svp = hv_fetch (hv, \"t\", 1, 0);
+ if (!svp || !*svp)
+ croak (\"missing 't' in hash\");
+ ret->t = SvIV (*svp);
+
+ svp = hv_fetch (hv, \"value\", 5, 0);
+ if (!svp || !*svp)
+ croak (\"missing 'value' in hash\");
+ ret->value = SvPV (*svp, ret->len);
+
+ return ret;
+}
+
MODULE = Win::Hivex PACKAGE = Win::Hivex
PROTOTYPES: ENABLE
@@ -2319,6 +2388,8 @@ DESTROY (h)
| AUnusedFlags -> ()
| ASetValues ->
pr " pl_set_values values = unpack_pl_set_values (ST(%d));\n" i
+ | ASetValue ->
+ pr " hive_set_value *val = unpack_set_value (ST(%d));\n" i
) (snd style);
let free_args () =
@@ -2326,6 +2397,8 @@ DESTROY (h)
function
| ASetValues ->
pr " free (values.values);\n"
+ | ASetValue ->
+ pr " free (val);\n"
| AHive | ANode _ | AValue _ | AString _ | AStringNullable _
| AOpenFlags | AUnusedFlags -> ()
) (snd style)
diff --git a/lib/hivex.c b/lib/hivex.c
index 74a7f55..fd644dc 100644
--- a/lib/hivex.c
+++ b/lib/hivex.c
@@ -2606,3 +2606,93 @@ hivex_node_set_values (hive_h *h, hive_node_h node,
return 0;
}
+
+
+int
+hivex_node_set_value (hive_h *h, hive_node_h node,
+ const hive_set_value *val, int flags)
+{
+ if (!h->writable) {
+ errno = EROFS;
+ return -1;
+ }
+
+ if (!IS_VALID_BLOCK (h, node) || !BLOCK_ID_EQ (h, node, "nk")) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ hive_value_h *prev_values = hivex_node_values (h, node);
+ if (prev_values == NULL)
+ return -1;
+
+ int retval = -1;
+
+ size_t nr_values = 0;
+ for (hive_value_h *itr = prev_values; *itr != 0; ++itr)
+ ++nr_values;
+
+ hive_set_value *values = malloc ((nr_values + 1) * (sizeof (hive_set_value)));
+ if (values == NULL)
+ goto leave_prev_values;
+
+ int alloc_ct = 0;
+ int idx_of_val = -1;
+ for (hive_value_h *prev_val = prev_values; *prev_val != 0; ++prev_val) {
+ size_t len;
+ hive_type t;
+
+ hive_set_value *value = &values[prev_val - prev_values];
+
+ char *valval = hivex_value_value (h, *prev_val, &t, &len);
+ if (valval == NULL) goto leave_partial;
+
+ ++alloc_ct;
+ value->value = valval;
+ value->t = t;
+ value->len = len;
+
+ char *valkey = hivex_value_key (h, *prev_val);
+ if (valkey == NULL) goto leave_partial;
+
+ ++alloc_ct;
+ value->key = valkey;
+
+ if (STRCASEEQ (valkey, val->key))
+ idx_of_val = prev_val - prev_values;
+ }
+
+ if (idx_of_val > -1) {
+ free (values[idx_of_val].key);
+ free (values[idx_of_val].value);
+ } else {
+ idx_of_val = nr_values;
+ ++nr_values;
+ }
+
+ hive_set_value *value = &values[idx_of_val];
+ *value = (hive_set_value){
+ .key = strdup (val->key),
+ .value = malloc (val->len),
+ .len = val->len,
+ .t = val->t
+ };
+
+ if (value->key == NULL || value->value == NULL) goto leave_partial;
+ memcpy (value->value, val->value, val->len);
+
+ retval = hivex_node_set_values (h, node, nr_values, values, 0);
+
+ leave_partial:
+ for (int i = 0; i < alloc_ct; i += 2) {
+ if (values[i / 2].value != NULL)
+ free (values[i / 2].value);
+ if (i + 1 < alloc_ct && values[i / 2].key != NULL)
+ free (values[i / 2].key);
+ }
+ free (values);
+
+ leave_prev_values:
+ free (prev_values);
+ return retval;
+}
diff --git a/perl/t/201-setvalue.t b/perl/t/201-setvalue.t
new file mode 100644
index 0000000..2504a5c
--- /dev/null
+++ b/perl/t/201-setvalue.t
@@ -0,0 +1,54 @@
+# hivex Perl bindings -*- perl -*-
+# Copyright (C) 2010 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+use strict;
+use warnings;
+use Test::More tests => 7;
+
+use Win::Hivex;
+
+my $srcdir = $ENV{srcdir} || ".";
+
+my $h = Win::Hivex->open ("$srcdir/../images/minimal", write => 1);
+ok ($h);
+
+my $root = $h->root ();
+ok ($root);
+
+$h->node_add_child ($root, "B");
+ok (1);
+
+my $b = $h->node_get_child ($root, "B");
+ok ($b);
+
+my $values = [
+ { key => "Key1", t => 3, value => "ABC" },
+ { key => "Key2", t => 3, value => "DEF" }
+ ];
+$h->node_set_values ($b, $values);
+ok (1);
+
+my $value1 = { key => "Key3", t => 3, value => "GHI" };
+$h->node_set_value ($b, $value1);
+ok (1);
+
+my $value2 = { key => "Key1", t => 3, value => "JKL" };
+$h->node_set_value ($b, $value2);
+ok (1);
+
+# don't commit because that would overwrite the original file
+# $h->commit ();
--
1.7.1
14 years, 3 months
[PATCH] hivex: add hivex_set_value api call and perl bindings, tests
by Conrad Meyer
Added Perl binding glue and a simple test along the lines of present
tests.
(And again: I'm not on the list, please CC me on replies. Thanks!)
---
generator/generator.ml | 62 +++++++++++++++++++++++++++++++--
lib/hivex.c | 90 ++++++++++++++++++++++++++++++++++++++++++++++++
perl/t/201-setvalue.t | 54 ++++++++++++++++++++++++++++
3 files changed, 203 insertions(+), 3 deletions(-)
create mode 100644 perl/t/201-setvalue.t
diff --git a/generator/generator.ml b/generator/generator.ml
index 5a0ab6e..70c91f2 100755
--- a/generator/generator.ml
+++ b/generator/generator.ml
@@ -71,6 +71,7 @@ and argt = (* Note, cannot be NULL/0 unless it
| AOpenFlags (* HIVEX_OPEN_* flags list. *)
| AUnusedFlags (* Flags arg that is always 0 *)
| ASetValues (* See hivex_node_set_values. *)
+ | ASetValue (* See hivex_node_set_value. *)
(* Hive types, from:
* https://secure.wikimedia.org/wikipedia/en/wiki/Windows_Registry#Keys_and_...
@@ -304,8 +305,15 @@ subnodes become invalid. You cannot delete the root node.";
"set (key, value) pairs at a node",
"\
This call can be used to set all the (key, value) pairs
-stored in C<node>. Note that this library does not offer
-a way to modify just a single key at a node.
+stored in C<node>.
+
+C<node> is the node to modify.";
+
+ "node_set_value", (RErr, [AHive; ANode "node"; ASetValue; AUnusedFlags]),
+ "set a single (key, value) pair at a given node",
+ "\
+This call can be used to set a single (key, value) pair
+stored in C<node>.
C<node> is the node to modify.";
]
@@ -459,6 +467,7 @@ let name_of_argt = function
| ANode n | AValue n | AString n | AStringNullable n -> n
| AOpenFlags | AUnusedFlags -> "flags"
| ASetValues -> "values"
+ | ASetValue -> "val"
(* Check function names etc. for consistency. *)
let check_functions () =
@@ -806,6 +815,7 @@ and generate_c_prototype ?(extern = false) name style =
| AString n | AStringNullable n -> pr "const char *%s" n
| AOpenFlags | AUnusedFlags -> pr "int flags"
| ASetValues -> pr "size_t nr_values, const hive_set_value *values"
+ | ASetValue -> pr "const hive_set_value *val"
) (snd style);
(match fst style with
| RLenType | RLenTypeVal -> pr ", hive_type *t, size_t *len"
@@ -937,6 +947,11 @@ Any existing values stored at the node are discarded, and their
C<hive_value_h> handles become invalid. Thus you can remove all
values stored at C<node> by passing C<nr_values = 0>.\n\n";
+ if List.mem ASetValue (snd style) then
+ pr "C<value> is a single (key, value) pair.
+
+Existing C<hive_value_h> handles become invalid.\n\n";
+
(match fst style with
| RErr ->
pr "\
@@ -1478,6 +1493,7 @@ and generate_ocaml_prototype ?(is_external = false) name style =
| AOpenFlags -> pr "open_flag list -> "
| AUnusedFlags -> ()
| ASetValues -> pr "set_value array -> "
+ | ASetValue -> pr "set_value -> "
) (snd style);
(match fst style with
| RErr -> pr "unit" (* all errors are turned into exceptions *)
@@ -1621,6 +1637,8 @@ static void raise_closed (const char *) Noreturn;
| ASetValues ->
pr " int nrvalues = Wosize_val (valuesv);\n";
pr " hive_set_value *values = HiveSetValues_val (valuesv);\n"
+ | ASetValue ->
+ pr " hive_set_value *val = HiveSetValue_val (valv);\n"
) (snd style);
pr "\n";
@@ -1684,7 +1702,7 @@ static void raise_closed (const char *) Noreturn;
List.iter (
function
| AHive | ANode _ | AValue _ | AString _ | AStringNullable _
- | AOpenFlags | AUnusedFlags -> ()
+ | AOpenFlags | AUnusedFlags | ASetValue -> ()
| ASetValues ->
pr " free (values);\n";
pr "\n";
@@ -2113,6 +2131,7 @@ and generate_perl_prototype name style =
| AOpenFlags -> pr "[flags]"
| AUnusedFlags -> assert false
| ASetValues -> pr "\\@values"
+ | ASetValue -> pr "$val"
) args;
pr ")"
@@ -2243,6 +2262,39 @@ unpack_pl_set_values (SV *sv)
return ret;
}
+static hive_set_value *
+unpack_set_value (SV *sv)
+{
+ hive_set_value *ret;
+
+ if (!sv || !SvROK (sv) || SvTYPE (SvRV (sv)) != SVt_PVHV)
+ croak (\"not a hash ref\");
+
+ ret = malloc (sizeof (hive_set_value));
+ if (ret == NULL)
+ croak (\"malloc failed\");
+
+ HV *hv = (HV *)SvRV(sv);
+
+ SV **svp;
+ svp = hv_fetch (hv, \"key\", 3, 0);
+ if (!svp || !*svp)
+ croak (\"missing 'key' in hash\");
+ ret->key = SvPV_nolen (*svp);
+
+ svp = hv_fetch (hv, \"t\", 1, 0);
+ if (!svp || !*svp)
+ croak (\"missing 't' in hash\");
+ ret->t = SvIV (*svp);
+
+ svp = hv_fetch (hv, \"value\", 5, 0);
+ if (!svp || !*svp)
+ croak (\"missing 'value' in hash\");
+ ret->value = SvPV (*svp, ret->len);
+
+ return ret;
+}
+
MODULE = Win::Hivex PACKAGE = Win::Hivex
PROTOTYPES: ENABLE
@@ -2319,6 +2371,8 @@ DESTROY (h)
| AUnusedFlags -> ()
| ASetValues ->
pr " pl_set_values values = unpack_pl_set_values (ST(%d));\n" i
+ | ASetValue ->
+ pr " hive_set_value *val = unpack_set_value (ST(%d));\n" i
) (snd style);
let free_args () =
@@ -2326,6 +2380,8 @@ DESTROY (h)
function
| ASetValues ->
pr " free (values.values);\n"
+ | ASetValue ->
+ pr " free (val);\n"
| AHive | ANode _ | AValue _ | AString _ | AStringNullable _
| AOpenFlags | AUnusedFlags -> ()
) (snd style)
diff --git a/lib/hivex.c b/lib/hivex.c
index 74a7f55..fd644dc 100644
--- a/lib/hivex.c
+++ b/lib/hivex.c
@@ -2606,3 +2606,93 @@ hivex_node_set_values (hive_h *h, hive_node_h node,
return 0;
}
+
+
+int
+hivex_node_set_value (hive_h *h, hive_node_h node,
+ const hive_set_value *val, int flags)
+{
+ if (!h->writable) {
+ errno = EROFS;
+ return -1;
+ }
+
+ if (!IS_VALID_BLOCK (h, node) || !BLOCK_ID_EQ (h, node, "nk")) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ hive_value_h *prev_values = hivex_node_values (h, node);
+ if (prev_values == NULL)
+ return -1;
+
+ int retval = -1;
+
+ size_t nr_values = 0;
+ for (hive_value_h *itr = prev_values; *itr != 0; ++itr)
+ ++nr_values;
+
+ hive_set_value *values = malloc ((nr_values + 1) * (sizeof (hive_set_value)));
+ if (values == NULL)
+ goto leave_prev_values;
+
+ int alloc_ct = 0;
+ int idx_of_val = -1;
+ for (hive_value_h *prev_val = prev_values; *prev_val != 0; ++prev_val) {
+ size_t len;
+ hive_type t;
+
+ hive_set_value *value = &values[prev_val - prev_values];
+
+ char *valval = hivex_value_value (h, *prev_val, &t, &len);
+ if (valval == NULL) goto leave_partial;
+
+ ++alloc_ct;
+ value->value = valval;
+ value->t = t;
+ value->len = len;
+
+ char *valkey = hivex_value_key (h, *prev_val);
+ if (valkey == NULL) goto leave_partial;
+
+ ++alloc_ct;
+ value->key = valkey;
+
+ if (STRCASEEQ (valkey, val->key))
+ idx_of_val = prev_val - prev_values;
+ }
+
+ if (idx_of_val > -1) {
+ free (values[idx_of_val].key);
+ free (values[idx_of_val].value);
+ } else {
+ idx_of_val = nr_values;
+ ++nr_values;
+ }
+
+ hive_set_value *value = &values[idx_of_val];
+ *value = (hive_set_value){
+ .key = strdup (val->key),
+ .value = malloc (val->len),
+ .len = val->len,
+ .t = val->t
+ };
+
+ if (value->key == NULL || value->value == NULL) goto leave_partial;
+ memcpy (value->value, val->value, val->len);
+
+ retval = hivex_node_set_values (h, node, nr_values, values, 0);
+
+ leave_partial:
+ for (int i = 0; i < alloc_ct; i += 2) {
+ if (values[i / 2].value != NULL)
+ free (values[i / 2].value);
+ if (i + 1 < alloc_ct && values[i / 2].key != NULL)
+ free (values[i / 2].key);
+ }
+ free (values);
+
+ leave_prev_values:
+ free (prev_values);
+ return retval;
+}
diff --git a/perl/t/201-setvalue.t b/perl/t/201-setvalue.t
new file mode 100644
index 0000000..2504a5c
--- /dev/null
+++ b/perl/t/201-setvalue.t
@@ -0,0 +1,54 @@
+# hivex Perl bindings -*- perl -*-
+# Copyright (C) 2010 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+use strict;
+use warnings;
+use Test::More tests => 7;
+
+use Win::Hivex;
+
+my $srcdir = $ENV{srcdir} || ".";
+
+my $h = Win::Hivex->open ("$srcdir/../images/minimal", write => 1);
+ok ($h);
+
+my $root = $h->root ();
+ok ($root);
+
+$h->node_add_child ($root, "B");
+ok (1);
+
+my $b = $h->node_get_child ($root, "B");
+ok ($b);
+
+my $values = [
+ { key => "Key1", t => 3, value => "ABC" },
+ { key => "Key2", t => 3, value => "DEF" }
+ ];
+$h->node_set_values ($b, $values);
+ok (1);
+
+my $value1 = { key => "Key3", t => 3, value => "GHI" };
+$h->node_set_value ($b, $value1);
+ok (1);
+
+my $value2 = { key => "Key1", t => 3, value => "JKL" };
+$h->node_set_value ($b, $value2);
+ok (1);
+
+# don't commit because that would overwrite the original file
+# $h->commit ();
--
1.7.1
14 years, 3 months
[PATCH 0/3] RFC: Allow use of external QEMU process with libguestfs
by Daniel P. Berrange
This attempts to implement the idea proposed in
https://www.redhat.com/archives/libguestfs/2010-April/msg00087.html
The idea is that an externally managed QEMU (manual, or via libvirt)
can boot the appliance kernel/initrd. libguestfs can then be just told
of the UNIX domain socket associated with the guest daemon.
An example based on guestfish.
1. Step one, find the appliance kernel/initrd (building the supermin
appliance image if neccessary)
><fs> find-appliance
><fs> get-kernel
/tmp/libguestfsqJB1iN/kernel
><fs> get-initrd
/tmp/libguestfsqJB1iN/initrd
2. Boot a QEMU instance with this info
/usr/libexec/qemu-kvm
-drive file=/dev/HostVG/f11i386,cache=off,if=virtio
-enable-kvm -nodefaults -nographic
-serial file:/tmp/guest/boot.log -monitor stdio
-m 500 -no-reboot
-chardev socket,id=guestfsvmc,path=/tmp/guest/sock,server,nowait
-net user,vlan=0,net=169.254.0.0/16,guestfwd=tcp:169.254.2.4:6666-chardev:guestfsvmc
-net nic,model=virtio,vlan=0
-append 'panic=1 console=ttyS0 udevtimeout=300 noapic acpi=off printk.time=1 cgroup_disable=memory selinux=0 guestfs_vmchannel=tcp:169.254.2.4:6666 TERM=xterm'
-kernel /tmp/libguestfsqJB1iN/kernel
-initrd /tmp/libguestfsqJB1iN/initrd
3. Tell guestfish to connect to this instance
><fs> launch-method attach
><fs> sockpath /tmp/guest/sock
><fs> launch
The temporary kernel/initrd from 'find-appliance' will be
automatically deleted when the guestfs handle is closed.
Instead of using 'find-appliance' an app can manually invoke
the febootstrap-supermin-helper to build the initrd/kernel
but is a fragile coupling to libguestfs internals. Thus it is
preferable to let libguestfs locate & inform you of the kernl
and initrd
14 years, 4 months
Progress bars
by Richard W.M. Jones
Background
----------
A complaint I'm hearing is that some tools which take a long time to
run (virt-resize in particular) need to have progress bars to indicate
how long they are expected to run. This is also a basic usability
principle, see for example this paper:
http://www.chrisharrison.net/projects/progressbars/ProgBarHarrison.pdf
If you look at how virt-resize is implemented, the bulk of the time is
spent copying partitions in a single long-running function, copy_size:
http://libguestfs.org/guestfs.3.html#guestfs_copy_size
The current protocol is entirely synchronous so that while the daemon
is doing the copy_size, the library is blocked waiting for a reply
message and does nothing else in that thread. To implement a progress
bar for this call we'd have to have a way to query into the call while
it was running to find out how far it has got, or to have it send
regular status messages out. The plan below outlines a way to do
this.
Protocol changes
----------------
This plan does require changing the protocol. We're allowed to change
the library/daemon protocol (it's not ABI) but we tend to avoid doing
it because it means people can't repackage our appliance and use it in
other distributions (see http://libguestfs.org/FAQ.html#distros). So
if we do change it now, we should:
(a) Only do it at the start of the 1.5 development cycle, indicating
that now would be a good time to release 1.4. See:
https://www.redhat.com/archives/libguestfs/2010-June/msg00069.html
(b) We should make the other protocol changes we've been wanting to
do, see: http://libguestfs.org/guestfs.3.html#libguestfs_gotchas
(c) We need to make sure everyone understands the change to the
appliance and protocol.
The current protocol (ignoring file transfers) implements a very
simple RPC mechanism which is entirely synchronous:
library daemon
request message --------------->
daemon processes message
<----------------- reply message
I have discarded the idea that we should change to using an
asynchronous system, eg. allowing the library to issue more than one
request message simultaneously, because at this stage it adds great
complexity to both ends, and is I believe not necessary in order to
implement progress messages. This means that we can't make additional
"get status" requests during a call.
Instead I'd like to change the protocol like this:
library daemon
request message --------------->
daemon processes message
<---------------- status message
daemon processes message
<---------------- status message
daemon processes message
<----------------- reply message
In a long running call such as copy_size, the daemon would send
periodic status messages to the library.
Points to note:
(i) The daemon should self-limit these messages, eg. to once per
second, starting at least one second after the request.
(ii) An initial implementation of the library would simply discard
these status messages. This would allow us to test the daemon side,
only making trivial library changes, and be reasonably sure that the
daemon side is correct.
(iii) The status message contains just two (64 bit) numbers:
- Total size of the current operation
- Current progress (0 <= current <= total size)
The meaning of these two numbers is defined by the context of the
call, but would usually indicate, eg. total size in bytes and number
of bytes processed/copied so far. Callers are only interested in
the ratio of these two numbers when displaying a progress meter.
Library changes
---------------
On receiving a status message, the library can ignore it, or can:
(1) update the total and current fields in the guestfs_h handle, and/or
(2) call a prearranged callback function.
We would add a way to query these numbers for an existing handle:
void guestfs_get_progress (guestfs_h *g, int64_t *total, int64_t *current);
Callers can poll this function on any open handle from any thread to
retrieve the progress of the currently running call.
[Side note: In general you cannot call libguestfs APIs from multiple
threads:
http://libguestfs.org/guestfs.3.html#multiple_handles_and_multiple_threads]
Also callers may register a callback function using:
typedef void (*guestfs_progress_cb) (guestfs_h *g, void *data,
int64_t total, int64_t current);
extern void guestfs_set_progress_callback (guestfs_h *g,
guestfs_progress_cb cb, void *data);
which is called on receipt of a status message.
The numbers are reset to (-1, -1) when the final reply is received for
a call. The numbers are only meaningful when the handle state is busy
(see http://libguestfs.org/guestfs.3.html#guestfs_is_busy).
All access to the numbers should be thread safe. Access to the busy
state should be made thread safe.
Note that we already implement thread safety using weak pthread
function calls in libguestfs (via gnulib), so this doesn't add any new
dependencies.
Daemon changes
--------------
Long running calls tend to be of two forms:
while (n < size) {
copy_a_buffer ();
n += size_copied;
}
or:
command ("long_running_external_command");
The first form can be changed easily:
while (n < size) {
copy_a_buffer ();
n += size_copied;
notify_progress (n, size);
}
The 'notify_progress' function can be called as often as needed, and
it would have to contain its own rate limiting functionality so that
it doesn't actually send progress messages back more often than
desired (eg. once per second). This allows for a very simple change
to all the potentially long-running daemon functions of the first
form.
The second form are more difficult to change. With each one we would
have to consider the nature of the external command, if it provides
some sort of progress feature or if we need to poll it from the daemon
(eg. polling the size of the input and output files).
Some functions of the second form could be changed. eg. The
implementation of guestfs_dd runs the external "dd" command, but could
be modified to an internal copy function of the first form, and this
might have other benefits too.
Other functions of the second form would be changed to poll status
over time. There is nothing in the library API which requires any
function to provide status messages.
Tools
-----
Tools, such as guestfish and virt-resize, must also be changed to poll
for status or set a callback for some long-running operations.
Obviously setting a callback would be preferred.
We would need to change the Perl and OCaml bindings at least to
support the progress callback.
Generator
---------
After making these changes we'd have a few API calls which generate
status messages, and many calls which don't. You can call
guestfs_get_progress on any call, but it wouldn't return any useful
information for the majority of calls.
It would be useful to mark those calls which generate status messages
in the generator. This would allow us to:
(a) Generate documentation.
(b) Modify guestfish to print progress bars for long-running calls.
Summary
-------
The above plan allows progress bars to be added to long-running
operations in our tools. The changes are relatively non-invasive to
all parts of libguestfs. However we do need to change the protocol,
and we should only consider doing this at the start of an unstable
development cycle.
Known deficiencies
------------------
guestfs_launch() cannot be metered this way. In fact there's no way
to know how long the launch operation will take, and the daemon [by
definition] is not running during this time anyway.
It sends extra messages (short ones) over the socket, and these
messages may be ignored by the library.
Rich.
--
Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones
virt-p2v converts physical machines to virtual machines. Boot with a
live CD or over the network (PXE) and turn machines into Xen guests.
http://et.redhat.com/~rjones/virt-p2v
14 years, 4 months
[PATCH] hivex: add hivex_set_value api call
by Conrad Meyer
I'm not entirely sure the generator/generator.ml changes are as correct
as they could be. I'm not very familiar with Caml.
The hivex_node_set_value call builds up a list of hive_set_values by
walking the existing values at the node, adding or replacing the passed
hive_set_value as necessary, then shoving the list at
hivex_node_set_values.
Not included: Perl or OCaml binding glue.
I'm not on the list, please CC me to replies. Thanks!
---
generator/generator.ml | 29 +++++++++++++--
lib/hivex.c | 90 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+), 4 deletions(-)
diff --git a/generator/generator.ml b/generator/generator.ml
index 5a0ab6e..dbcbd2c 100755
--- a/generator/generator.ml
+++ b/generator/generator.ml
@@ -71,6 +71,7 @@ and argt = (* Note, cannot be NULL/0 unless it
| AOpenFlags (* HIVEX_OPEN_* flags list. *)
| AUnusedFlags (* Flags arg that is always 0 *)
| ASetValues (* See hivex_node_set_values. *)
+ | ASetValue (* See hivex_node_set_value. *)
(* Hive types, from:
* https://secure.wikimedia.org/wikipedia/en/wiki/Windows_Registry#Keys_and_...
@@ -304,8 +305,15 @@ subnodes become invalid. You cannot delete the root node.";
"set (key, value) pairs at a node",
"\
This call can be used to set all the (key, value) pairs
-stored in C<node>. Note that this library does not offer
-a way to modify just a single key at a node.
+stored in C<node>.
+
+C<node> is the node to modify.";
+
+ "node_set_value", (RErr, [AHive; ANode "node"; ASetValue; AUnusedFlags]),
+ "set a single (key, value) pair at a given node",
+ "\
+This call can be used to set a single (key, value) pair
+stored in C<node>.
C<node> is the node to modify.";
]
@@ -459,6 +467,7 @@ let name_of_argt = function
| ANode n | AValue n | AString n | AStringNullable n -> n
| AOpenFlags | AUnusedFlags -> "flags"
| ASetValues -> "values"
+ | ASetValue -> "val"
(* Check function names etc. for consistency. *)
let check_functions () =
@@ -806,6 +815,7 @@ and generate_c_prototype ?(extern = false) name style =
| AString n | AStringNullable n -> pr "const char *%s" n
| AOpenFlags | AUnusedFlags -> pr "int flags"
| ASetValues -> pr "size_t nr_values, const hive_set_value *values"
+ | ASetValue -> pr "const hive_set_value *val"
) (snd style);
(match fst style with
| RLenType | RLenTypeVal -> pr ", hive_type *t, size_t *len"
@@ -937,6 +947,11 @@ Any existing values stored at the node are discarded, and their
C<hive_value_h> handles become invalid. Thus you can remove all
values stored at C<node> by passing C<nr_values = 0>.\n\n";
+ if List.mem ASetValue (snd style) then
+ pr "C<value> is a single (key, value) pair.
+
+Existing C<hive_value_h> handles become invalid.\n\n";
+
(match fst style with
| RErr ->
pr "\
@@ -1478,6 +1493,7 @@ and generate_ocaml_prototype ?(is_external = false) name style =
| AOpenFlags -> pr "open_flag list -> "
| AUnusedFlags -> ()
| ASetValues -> pr "set_value array -> "
+ | ASetValue -> pr "set_value -> "
) (snd style);
(match fst style with
| RErr -> pr "unit" (* all errors are turned into exceptions *)
@@ -1621,6 +1637,8 @@ static void raise_closed (const char *) Noreturn;
| ASetValues ->
pr " int nrvalues = Wosize_val (valuesv);\n";
pr " hive_set_value *values = HiveSetValues_val (valuesv);\n"
+ | ASetValue ->
+ pr " hive_set_value *val = HiveSetValue_val (valv);\n"
) (snd style);
pr "\n";
@@ -1684,7 +1702,7 @@ static void raise_closed (const char *) Noreturn;
List.iter (
function
| AHive | ANode _ | AValue _ | AString _ | AStringNullable _
- | AOpenFlags | AUnusedFlags -> ()
+ | AOpenFlags | AUnusedFlags | ASetValue -> ()
| ASetValues ->
pr " free (values);\n";
pr "\n";
@@ -2113,6 +2131,7 @@ and generate_perl_prototype name style =
| AOpenFlags -> pr "[flags]"
| AUnusedFlags -> assert false
| ASetValues -> pr "\\@values"
+ | ASetValue -> pr "$val"
) args;
pr ")"
@@ -2319,6 +2338,8 @@ DESTROY (h)
| AUnusedFlags -> ()
| ASetValues ->
pr " pl_set_values values = unpack_pl_set_values (ST(%d));\n" i
+ | ASetValue ->
+ pr " pl_set_value val = unpack_pl_set_value (ST(%d));\n" i
) (snd style);
let free_args () =
@@ -2327,7 +2348,7 @@ DESTROY (h)
| ASetValues ->
pr " free (values.values);\n"
| AHive | ANode _ | AValue _ | AString _ | AStringNullable _
- | AOpenFlags | AUnusedFlags -> ()
+ | AOpenFlags | AUnusedFlags | ASetValue -> ()
) (snd style)
in
diff --git a/lib/hivex.c b/lib/hivex.c
index 74a7f55..3879238 100644
--- a/lib/hivex.c
+++ b/lib/hivex.c
@@ -2606,3 +2606,93 @@ hivex_node_set_values (hive_h *h, hive_node_h node,
return 0;
}
+
+
+int
+hivex_node_set_value (hive_h *h, hive_node_h node,
+ const hive_set_value *val, int flags)
+{
+ if (!h->writable) {
+ errno = EROFS;
+ return -1;
+ }
+
+ if (!IS_VALID_BLOCK (h, node) || !BLOCK_ID_EQ (h, node, "nk")) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ hive_value_h *prev_values = hivex_node_values (h, node);
+ if (prev_values == NULL)
+ return -1;
+
+ int retval = -1;
+
+ size_t nr_values = 0;
+ for (hive_value_h *itr = prev_values; *itr != 0; ++itr)
+ ++nr_values;
+
+ hive_set_value *values = malloc ((nr_values + 1) * (sizeof (hive_set_value)));
+ if (values == NULL)
+ goto leave_prev_values;
+
+ int alloc_ct = 0;
+ int idx_of_val = -1;
+ for (hive_value_h *prev_val = prev_values; *prev_val != 0; ++prev_val) {
+ size_t len;
+ hive_type t;
+
+ hive_set_value *value = &values[prev_val - prev_values];
+
+ char *valval = hivex_value_value (h, *prev_val, &t, &len);
+ if (valval == NULL) goto leave_partial;
+
+ ++alloc_ct;
+ value->value = valval;
+ value->t = t;
+ value->len = len;
+
+ char *valkey = hivex_value_key (h, *prev_val);
+ if (valkey == NULL) goto leave_partial;
+
+ ++alloc_ct;
+ value->key = valkey;
+
+ if (strcmp (valkey, val->key) == 0)
+ idx_of_val = prev_val - prev_values;
+ }
+
+ if (idx_of_val > -1) {
+ free (values[idx_of_val].key);
+ free (values[idx_of_val].value);
+ } else {
+ idx_of_val = nr_values;
+ ++nr_values;
+ }
+
+ hive_set_value *value = &values[idx_of_val];
+ *value = (hive_set_value){
+ .key = strdup (val->key),
+ .value = malloc (val->len),
+ .len = val->len,
+ .t = val->t
+ };
+
+ if (value->key == NULL || value->value == NULL) goto leave_partial;
+ memcpy (value->value, val->value, val->len);
+
+ retval = hivex_node_set_values (h, node, nr_values, values, 0);
+
+ leave_partial:
+ for (int i = 0; i < alloc_ct; i += 2) {
+ if (values[i / 2].value != NULL)
+ free (values[i / 2].value);
+ if (i + 1 < alloc_ct && values[i / 2].key != NULL)
+ free (values[i / 2].key);
+ }
+ free (values);
+
+ leave_prev_values:
+ free (prev_values);
+ return retval;
+}
--
1.7.1
14 years, 4 months