Files
ostree/src/libostree/ostree-repo-static-delta-processing.c
T
Colin Walters 4c8fc92aa0 lib: Always checksum content in deltas
This is a follow up to conversation on list - in practice, if we're
backing away from summary signing, then it makes sense to remove the
special casing for checksums in deltas around summary signatures.

This is also related to the recent change to enable GPG checking for
commits in deltas - now we have a more coherent story between the
previous pull path and deltas.

I didn't do any performance checking, and while it's slightly annoying
that we're now doing sha256 on the delta content twice (once for the
part and once per object)...sha256 is pretty fast, I think most users
are I/O bound anyways, and it'd drop even farther if we started using
openssl.

Closes: #612
Approved by: jlebon
2016-12-06 15:59:35 +00:00

924 lines
28 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright (C) 2013,2014 Colin Walters <walters@verbum.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#include "config.h"
#include <string.h>
#include <glib-unix.h>
#include <gio/gunixinputstream.h>
#include <gio/gunixoutputstream.h>
#include <gio/gfiledescriptorbased.h>
#include "ostree-core-private.h"
#include "ostree-repo-private.h"
#include "ostree-repo-static-delta-private.h"
#include "ostree-lzma-decompressor.h"
#include "otutil.h"
#include "ostree-varint.h"
#include "bsdiff/bspatch.h"
/* This should really always be true, but hey, let's just assert it */
G_STATIC_ASSERT (sizeof (guint) >= sizeof (guint32));
typedef struct {
gboolean stats_only;
OstreeRepo *repo;
guint checksum_index;
const guint8 *checksums;
guint n_checksums;
const guint8 *opdata;
guint oplen;
GVariant *mode_dict;
GVariant *xattr_dict;
gboolean object_start;
gboolean caught_error;
GError **async_error;
OstreeObjectType output_objtype;
OstreeRepoContentBareCommit barecommitstate;
guint64 content_size;
GOutputStream *content_out;
GChecksum *content_checksum;
char checksum[OSTREE_SHA256_STRING_LEN+1];
char *read_source_object;
int read_source_fd;
gboolean have_obj;
guint32 uid;
guint32 gid;
guint32 mode;
GVariant *xattrs;
const guint8 *output_target;
const guint8 *input_target_csum;
const guint8 *payload_data;
guint64 payload_size;
} StaticDeltaExecutionState;
typedef struct {
StaticDeltaExecutionState *state;
char checksum[OSTREE_SHA256_STRING_LEN+1];
} StaticDeltaContentWrite;
typedef gboolean (*DispatchOpFunc) (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error);
#define OPPROTO(name) \
static gboolean dispatch_##name (OstreeRepo *repo, \
StaticDeltaExecutionState *state, \
GCancellable *cancellable, \
GError **error);
OPPROTO(open_splice_and_close)
OPPROTO(open)
OPPROTO(write)
OPPROTO(set_read_source)
OPPROTO(unset_read_source)
OPPROTO(close)
OPPROTO(bspatch)
#undef OPPROTO
static void
static_delta_execution_state_init (StaticDeltaExecutionState *state)
{
state->read_source_fd = -1;
}
static gboolean
read_varuint64 (StaticDeltaExecutionState *state,
guint64 *out_value,
GError **error)
{
gsize bytes_read;
if (!_ostree_read_varuint64 (state->opdata, state->oplen, out_value, &bytes_read))
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Unexpected EOF reading varint");
return FALSE;
}
state->opdata += bytes_read;
state->oplen -= bytes_read;
return TRUE;
}
static gboolean
open_output_target (StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
guint8 *objcsum;
g_assert (state->checksums != NULL);
g_assert (state->output_target == NULL);
g_assert (state->checksum_index < state->n_checksums);
objcsum = (guint8*)state->checksums + (state->checksum_index * OSTREE_STATIC_DELTA_OBJTYPE_CSUM_LEN);
if (G_UNLIKELY(!ostree_validate_structureof_objtype (*objcsum, error)))
goto out;
state->output_objtype = (OstreeObjectType) *objcsum;
state->output_target = objcsum + 1;
ostree_checksum_inplace_from_bytes (state->output_target, state->checksum);
ret = TRUE;
out:
return ret;
}
static guint
delta_opcode_index (OstreeStaticDeltaOpCode op)
{
switch (op)
{
case OSTREE_STATIC_DELTA_OP_OPEN_SPLICE_AND_CLOSE:
return 0;
case OSTREE_STATIC_DELTA_OP_OPEN:
return 1;
case OSTREE_STATIC_DELTA_OP_WRITE:
return 2;
case OSTREE_STATIC_DELTA_OP_SET_READ_SOURCE:
return 3;
case OSTREE_STATIC_DELTA_OP_UNSET_READ_SOURCE:
return 4;
case OSTREE_STATIC_DELTA_OP_CLOSE:
return 5;
case OSTREE_STATIC_DELTA_OP_BSPATCH:
return 6;
default:
g_assert_not_reached ();
}
}
gboolean
_ostree_static_delta_part_execute (OstreeRepo *repo,
GVariant *objects,
GVariant *part,
gboolean stats_only,
OstreeDeltaExecuteStats *stats,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
guint8 *checksums_data;
g_autoptr(GVariant) mode_dict = NULL;
g_autoptr(GVariant) xattr_dict = NULL;
g_autoptr(GVariant) payload = NULL;
g_autoptr(GVariant) ops = NULL;
StaticDeltaExecutionState statedata = { 0, };
StaticDeltaExecutionState *state = &statedata;
guint n_executed = 0;
static_delta_execution_state_init (&statedata);
state->repo = repo;
state->async_error = error;
state->stats_only = stats_only;
if (!_ostree_static_delta_parse_checksum_array (objects,
&checksums_data,
&state->n_checksums,
error))
goto out;
/* Skip processing for empty delta part */
if (state->n_checksums == 0)
{
ret = TRUE;
goto out;
}
state->checksums = checksums_data;
g_variant_get (part, "(@a(uuu)@aa(ayay)@ay@ay)",
&mode_dict,
&xattr_dict,
&payload, &ops);
state->mode_dict = mode_dict;
state->xattr_dict = xattr_dict;
state->payload_data = g_variant_get_data (payload);
state->payload_size = g_variant_get_size (payload);
state->oplen = g_variant_n_children (ops);
state->opdata = g_variant_get_data (ops);
while (state->oplen > 0)
{
guint8 opcode;
opcode = state->opdata[0];
state->oplen--;
state->opdata++;
switch (opcode)
{
case OSTREE_STATIC_DELTA_OP_OPEN_SPLICE_AND_CLOSE:
if (!dispatch_open_splice_and_close (repo, state, cancellable, error))
goto out;
break;
case OSTREE_STATIC_DELTA_OP_OPEN:
if (!dispatch_open (repo, state, cancellable, error))
goto out;
break;
case OSTREE_STATIC_DELTA_OP_WRITE:
if (!dispatch_write (repo, state, cancellable, error))
goto out;
break;
case OSTREE_STATIC_DELTA_OP_SET_READ_SOURCE:
if (!dispatch_set_read_source (repo, state, cancellable, error))
goto out;
break;
case OSTREE_STATIC_DELTA_OP_UNSET_READ_SOURCE:
if (!dispatch_unset_read_source (repo, state, cancellable, error))
goto out;
break;
case OSTREE_STATIC_DELTA_OP_CLOSE:
if (!dispatch_close (repo, state, cancellable, error))
goto out;
break;
case OSTREE_STATIC_DELTA_OP_BSPATCH:
if (!dispatch_bspatch (repo, state, cancellable, error))
goto out;
break;
default:
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Unknown opcode %u at offset %u", opcode, n_executed);
goto out;
}
n_executed++;
if (stats)
stats->n_ops_executed[delta_opcode_index(opcode)]++;
}
if (state->caught_error)
goto out;
ret = TRUE;
out:
g_clear_pointer (&state->content_checksum, g_checksum_free);
return ret;
}
typedef struct {
OstreeRepo *repo;
GVariant *header;
GVariant *part;
GCancellable *cancellable;
GSimpleAsyncResult *result;
} StaticDeltaPartExecuteAsyncData;
static void
static_delta_part_execute_async_data_free (gpointer user_data)
{
StaticDeltaPartExecuteAsyncData *data = user_data;
g_clear_object (&data->repo);
g_variant_unref (data->header);
g_variant_unref (data->part);
g_clear_object (&data->cancellable);
g_free (data);
}
static void
static_delta_part_execute_thread (GSimpleAsyncResult *res,
GObject *object,
GCancellable *cancellable)
{
GError *error = NULL;
StaticDeltaPartExecuteAsyncData *data;
data = g_simple_async_result_get_op_res_gpointer (res);
if (!_ostree_static_delta_part_execute (data->repo,
data->header,
data->part,
FALSE, NULL,
cancellable, &error))
g_simple_async_result_take_error (res, error);
}
void
_ostree_static_delta_part_execute_async (OstreeRepo *repo,
GVariant *header,
GVariant *part,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
StaticDeltaPartExecuteAsyncData *asyncdata;
asyncdata = g_new0 (StaticDeltaPartExecuteAsyncData, 1);
asyncdata->repo = g_object_ref (repo);
asyncdata->header = g_variant_ref (header);
asyncdata->part = g_variant_ref (part);
asyncdata->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
asyncdata->result = g_simple_async_result_new ((GObject*) repo,
callback, user_data,
_ostree_static_delta_part_execute_async);
g_simple_async_result_set_op_res_gpointer (asyncdata->result, asyncdata,
static_delta_part_execute_async_data_free);
g_simple_async_result_run_in_thread (asyncdata->result, static_delta_part_execute_thread, G_PRIORITY_DEFAULT, cancellable);
g_object_unref (asyncdata->result);
}
gboolean
_ostree_static_delta_part_execute_finish (OstreeRepo *repo,
GAsyncResult *result,
GError **error)
{
GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result);
g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == _ostree_static_delta_part_execute_async);
if (g_simple_async_result_propagate_error (simple, error))
return FALSE;
return TRUE;
}
static gboolean
validate_ofs (StaticDeltaExecutionState *state,
guint64 offset,
guint64 length,
GError **error)
{
if (G_UNLIKELY (offset + length < offset ||
offset + length > state->payload_size))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Invalid offset/length %" G_GUINT64_FORMAT "/%" G_GUINT64_FORMAT,
offset, length);
return FALSE;
}
return TRUE;
}
static gboolean
content_out_write (OstreeRepo *repo,
StaticDeltaExecutionState *state,
const guint8* buf,
gsize len,
GCancellable *cancellable,
GError **error)
{
gsize bytes_written;
if (state->content_checksum)
g_checksum_update (state->content_checksum, buf, len);
/* Ignore bytes_written since we discard partial content */
if (!g_output_stream_write_all (state->content_out,
buf, len,
&bytes_written,
cancellable, error))
return FALSE;
return TRUE;
}
static gboolean
do_content_open_generic (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
g_autoptr(GVariant) modev = NULL;
guint64 mode_offset;
guint64 xattr_offset;
guint32 uid, gid, mode;
if (!read_varuint64 (state, &mode_offset, error))
goto out;
if (!read_varuint64 (state, &xattr_offset, error))
goto out;
state->barecommitstate.fd = -1;
modev = g_variant_get_child_value (state->mode_dict, mode_offset);
g_variant_get (modev, "(uuu)", &uid, &gid, &mode);
state->uid = GUINT32_FROM_BE (uid);
state->gid = GUINT32_FROM_BE (gid);
state->mode = GUINT32_FROM_BE (mode);
state->xattrs = g_variant_get_child_value (state->xattr_dict, xattr_offset);
ret = TRUE;
out:
return ret;
}
struct bzpatch_opaque_s
{
StaticDeltaExecutionState *state;
guint64 offset, length;
};
static int
bspatch_read (const struct bspatch_stream* stream, void* buffer, int length)
{
struct bzpatch_opaque_s *opaque = stream->opaque;
g_assert (length <= opaque->length);
g_assert (opaque->offset + length <= opaque->state->payload_size);
memcpy (buffer, opaque->state->payload_data + opaque->offset, length);
opaque->offset += length;
opaque->length -= length;
return 0;
}
static gboolean
dispatch_bspatch (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
guint64 offset, length;
g_autoptr(GMappedFile) input_mfile = NULL;
g_autofree guchar *buf = NULL;
struct bspatch_stream stream;
struct bzpatch_opaque_s opaque;
if (!read_varuint64 (state, &offset, error))
goto out;
if (!read_varuint64 (state, &length, error))
goto out;
if (state->stats_only)
{
ret = TRUE;
goto out;
}
if (!state->have_obj)
{
input_mfile = g_mapped_file_new_from_fd (state->read_source_fd, FALSE, error);
if (!input_mfile)
goto out;
buf = g_malloc0 (state->content_size);
opaque.state = state;
opaque.offset = offset;
opaque.length = length;
stream.read = bspatch_read;
stream.opaque = &opaque;
if (bspatch ((const guint8*)g_mapped_file_get_contents (input_mfile),
g_mapped_file_get_length (input_mfile),
buf,
state->content_size,
&stream) < 0)
goto out;
if (!content_out_write (repo, state, buf, state->content_size,
cancellable, error))
goto out;
}
ret = TRUE;
out:
return ret;
}
/* Before, we had a distinction between "trusted" and "untrusted" deltas
* which we've decided wasn't a good idea. Now, we always checksum the content.
* Compare with what ostree_checksum_file_from_input() is doing too.
*/
static gboolean
handle_untrusted_content_checksum (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
g_autoptr(GVariant) header = NULL;
g_autoptr(GFileInfo) finfo = NULL;
gsize bytes_written;
finfo = _ostree_header_gfile_info_new (state->mode, state->uid, state->gid);
header = _ostree_file_header_new (finfo, state->xattrs);
state->content_checksum = g_checksum_new (G_CHECKSUM_SHA256);
if (!_ostree_write_variant_with_size (NULL, header, 0, &bytes_written, state->content_checksum,
cancellable, error))
return FALSE;
return TRUE;
}
static gboolean
dispatch_open_splice_and_close (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
if (!open_output_target (state, cancellable, error))
goto out;
if (OSTREE_OBJECT_TYPE_IS_META (state->output_objtype))
{
g_autoptr(GVariant) metadata = NULL;
guint64 offset;
guint64 length;
if (!read_varuint64 (state, &length, error))
goto out;
if (!read_varuint64 (state, &offset, error))
goto out;
if (!validate_ofs (state, offset, length, error))
goto out;
if (state->stats_only)
{
ret = TRUE;
goto out;
}
metadata = g_variant_new_from_data (ostree_metadata_variant_type (state->output_objtype),
state->payload_data + offset, length, TRUE, NULL, NULL);
{
g_autofree guchar *actual_csum = NULL;
if (!ostree_repo_write_metadata (state->repo, state->output_objtype,
state->checksum,
metadata, &actual_csum,
cancellable,
error))
goto out;
}
}
else
{
guint64 content_offset;
guint64 objlen;
g_autoptr(GInputStream) object_input = NULL;
g_autoptr(GInputStream) memin = NULL;
if (!do_content_open_generic (repo, state, cancellable, error))
goto out;
if (!read_varuint64 (state, &state->content_size, error))
goto out;
if (!read_varuint64 (state, &content_offset, error))
goto out;
if (!validate_ofs (state, content_offset, state->content_size, error))
goto out;
if (state->stats_only)
{
ret = TRUE;
goto out;
}
/* Fast path for regular files to bare repositories */
if (S_ISREG (state->mode) &&
(repo->mode == OSTREE_REPO_MODE_BARE ||
repo->mode == OSTREE_REPO_MODE_BARE_USER))
{
if (!_ostree_repo_open_content_bare (repo, state->checksum,
state->content_size,
&state->barecommitstate,
&state->content_out,
&state->have_obj,
cancellable, error))
goto out;
if (!state->have_obj)
{
if (!handle_untrusted_content_checksum (repo, state, cancellable, error))
goto out;
if (!content_out_write (repo, state,
state->payload_data + content_offset,
state->content_size,
cancellable, error))
goto out;
}
}
else
{
/* Slower path, for symlinks and unpacking deltas into archive-z2 */
g_autoptr(GFileInfo) finfo = NULL;
finfo = _ostree_header_gfile_info_new (state->mode, state->uid, state->gid);
if (S_ISLNK (state->mode))
{
g_autofree char *nulterminated_target =
g_strndup ((char*)state->payload_data + content_offset, state->content_size);
g_file_info_set_symlink_target (finfo, nulterminated_target);
}
else
{
g_assert (S_ISREG (state->mode));
g_file_info_set_size (finfo, state->content_size);
memin = g_memory_input_stream_new_from_data (state->payload_data + content_offset, state->content_size, NULL);
}
if (!ostree_raw_file_to_content_stream (memin, finfo, state->xattrs,
&object_input, &objlen,
cancellable, error))
goto out;
{
g_autofree guchar *actual_csum = NULL;
if (!ostree_repo_write_content (state->repo,
state->checksum,
object_input,
objlen,
&actual_csum,
cancellable,
error))
goto out;
}
}
}
if (!dispatch_close (repo, state, cancellable, error))
goto out;
ret = TRUE;
out:
if (state->stats_only)
(void) dispatch_close (repo, state, cancellable, NULL);
if (!ret)
g_prefix_error (error, "opcode open-splice-and-close: ");
return ret;
}
static gboolean
dispatch_open (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
g_assert (state->output_target == NULL);
/* FIXME - lift this restriction */
if (!state->stats_only)
{
g_assert (repo->mode == OSTREE_REPO_MODE_BARE ||
repo->mode == OSTREE_REPO_MODE_BARE_USER);
}
if (!open_output_target (state, cancellable, error))
goto out;
if (!do_content_open_generic (repo, state, cancellable, error))
goto out;
if (!read_varuint64 (state, &state->content_size, error))
goto out;
if (state->stats_only)
{
ret = TRUE;
goto out;
}
if (!_ostree_repo_open_content_bare (repo, state->checksum,
state->content_size,
&state->barecommitstate,
&state->content_out,
&state->have_obj,
cancellable, error))
goto out;
if (!handle_untrusted_content_checksum (repo, state, cancellable, error))
goto out;
ret = TRUE;
out:
if (!ret)
g_prefix_error (error, "opcode open: ");
return ret;
}
static gboolean
dispatch_write (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
guint64 content_size;
guint64 content_offset;
if (!read_varuint64 (state, &content_size, error))
goto out;
if (!read_varuint64 (state, &content_offset, error))
goto out;
if (state->stats_only)
{
ret = TRUE;
goto out;
}
if (!state->have_obj)
{
if (state->read_source_fd != -1)
{
if (lseek (state->read_source_fd, content_offset, SEEK_SET) == -1)
{
glnx_set_error_from_errno (error);
goto out;
}
while (content_size > 0)
{
char buf[4096];
gssize bytes_read;
do
bytes_read = read (state->read_source_fd, buf, MIN(sizeof(buf), content_size));
while (G_UNLIKELY (bytes_read == -1 && errno == EINTR));
if (bytes_read == -1)
{
glnx_set_error_from_errno (error);
goto out;
}
if (G_UNLIKELY (bytes_read == 0))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Unexpected EOF reading object %s", state->read_source_object);
goto out;
}
if (!content_out_write (repo, state, (guint8*)buf, bytes_read,
cancellable, error))
goto out;
content_size -= bytes_read;
}
}
else
{
if (!validate_ofs (state, content_offset, content_size, error))
goto out;
if (!content_out_write (repo, state, state->payload_data + content_offset, content_size,
cancellable, error))
goto out;
}
}
ret = TRUE;
out:
if (!ret)
g_prefix_error (error, "opcode open-splice-and-close: ");
return ret;
}
static gboolean
dispatch_set_read_source (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
guint64 source_offset;
if (state->read_source_fd != -1)
{
(void) close (state->read_source_fd);
state->read_source_fd = -1;
}
if (!read_varuint64 (state, &source_offset, error))
goto out;
if (!validate_ofs (state, source_offset, 32, error))
goto out;
if (state->stats_only)
{
ret = TRUE;
goto out;
}
g_free (state->read_source_object);
state->read_source_object = ostree_checksum_from_bytes (state->payload_data + source_offset);
if (!_ostree_repo_read_bare_fd (repo, state->read_source_object, &state->read_source_fd,
cancellable, error))
goto out;
ret = TRUE;
out:
if (!ret)
g_prefix_error (error, "opcode set-read-source: ");
return ret;
}
static gboolean
dispatch_unset_read_source (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
if (state->stats_only)
{
ret = TRUE;
goto out;
}
if (state->read_source_fd != -1)
{
(void) close (state->read_source_fd);
state->read_source_fd = -1;
}
g_clear_pointer (&state->read_source_object, g_free);
ret = TRUE;
out:
if (!ret)
g_prefix_error (error, "opcode unset-read-source: ");
return ret;
}
static gboolean
dispatch_close (OstreeRepo *repo,
StaticDeltaExecutionState *state,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
if (state->content_out)
{
if (!g_output_stream_flush (state->content_out, cancellable, error))
goto out;
if (state->content_checksum)
{
const char *actual_checksum = g_checksum_get_string (state->content_checksum);
if (strcmp (actual_checksum, state->checksum) != 0)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Corrupted object %s (actual checksum is %s)",
state->checksum, actual_checksum);
goto out;
}
}
if (!_ostree_repo_commit_trusted_content_bare (repo, state->checksum, &state->barecommitstate,
state->uid, state->gid, state->mode,
state->xattrs,
cancellable, error))
goto out;
}
if (!dispatch_unset_read_source (repo, state, cancellable, error))
goto out;
g_clear_pointer (&state->xattrs, g_variant_unref);
g_clear_pointer (&state->content_checksum, g_checksum_free);
g_clear_object (&state->content_out);
state->checksum_index++;
state->output_target = NULL;
ret = TRUE;
out:
if (!ret)
g_prefix_error (error, "opcode open-splice-and-close: ");
return ret;
}