blob: 3bcd0b84ee6183111b324380a2b371d3c91eb56a [file] [log] [blame]
/*
* Implementation of the Microsoft Installer (msi.dll)
*
* Copyright 2005 Mike McCormack for CodeWeavers
* Copyright 2005 Aric Stewart for CodeWeavers
*
* 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.1 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <stdarg.h>
#include <stdio.h>
#define COBJMACROS
#include "windef.h"
#include "winbase.h"
#include "winerror.h"
#include "wine/debug.h"
#include "msi.h"
#include "winnls.h"
#include "objbase.h"
#include "oleauto.h"
#include "msipriv.h"
#include "msiserver.h"
#include "wine/unicode.h"
WINE_DEFAULT_DEBUG_CHANNEL(msi);
/* types arranged by precedence */
#define FORMAT_NULL 0x0001
#define FORMAT_LITERAL 0x0002
#define FORMAT_NUMBER 0x0004
#define FORMAT_LBRACK 0x0010
#define FORMAT_LBRACE 0x0020
#define FORMAT_RBRACK 0x0011
#define FORMAT_RBRACE 0x0021
#define FORMAT_ESCAPE 0x0040
#define FORMAT_PROPNULL 0x0080
#define FORMAT_ERROR 0x1000
#define FORMAT_FAIL 0x2000
#define left_type(x) (x & 0xF0)
typedef struct _tagFORMAT
{
MSIPACKAGE *package;
MSIRECORD *record;
LPWSTR deformatted;
int len;
int n;
BOOL propfailed;
BOOL groupfailed;
int groups;
} FORMAT;
typedef struct _tagFORMSTR
{
struct list entry;
int n;
int len;
int type;
BOOL propfound;
BOOL nonprop;
} FORMSTR;
typedef struct _tagSTACK
{
struct list items;
} STACK;
static STACK *create_stack(void)
{
STACK *stack = msi_alloc(sizeof(STACK));
list_init(&stack->items);
return stack;
}
static void free_stack(STACK *stack)
{
while (!list_empty(&stack->items))
{
FORMSTR *str = LIST_ENTRY(list_head(&stack->items), FORMSTR, entry);
list_remove(&str->entry);
msi_free(str);
}
msi_free(stack);
}
static void stack_push(STACK *stack, FORMSTR *str)
{
list_add_head(&stack->items, &str->entry);
}
static FORMSTR *stack_pop(STACK *stack)
{
FORMSTR *ret;
if (list_empty(&stack->items))
return NULL;
ret = LIST_ENTRY(list_head(&stack->items), FORMSTR, entry);
list_remove(&ret->entry);
return ret;
}
static FORMSTR *stack_find(STACK *stack, int type)
{
FORMSTR *str;
LIST_FOR_EACH_ENTRY(str, &stack->items, FORMSTR, entry)
{
if (str->type == type)
return str;
}
return NULL;
}
static FORMSTR *stack_peek(STACK *stack)
{
return LIST_ENTRY(list_head(&stack->items), FORMSTR, entry);
}
static LPCWSTR get_formstr_data(FORMAT *format, FORMSTR *str)
{
return &format->deformatted[str->n];
}
static WCHAR *dup_formstr( FORMAT *format, FORMSTR *str, int *ret_len )
{
WCHAR *val;
if (!str->len) return NULL;
if ((val = msi_alloc( (str->len + 1) * sizeof(WCHAR) )))
{
memcpy( val, get_formstr_data(format, str), str->len * sizeof(WCHAR) );
val[str->len] = 0;
*ret_len = str->len;
}
return val;
}
static WCHAR *deformat_index( FORMAT *format, FORMSTR *str, int *ret_len )
{
WCHAR *val, *ret;
DWORD len;
int field;
if (!(val = msi_alloc( (str->len + 1) * sizeof(WCHAR) ))) return NULL;
lstrcpynW(val, get_formstr_data(format, str), str->len + 1);
field = atoiW( val );
msi_free( val );
if (MSI_RecordIsNull( format->record, field ) ||
MSI_RecordGetStringW( format->record, field, NULL, &len )) return NULL;
len++;
if (!(ret = msi_alloc( len * sizeof(WCHAR) ))) return NULL;
ret[0] = 0;
if (MSI_RecordGetStringW( format->record, field, ret, &len ))
{
msi_free( ret );
return NULL;
}
*ret_len = len;
return ret;
}
static WCHAR *deformat_property( FORMAT *format, FORMSTR *str, int *ret_len )
{
WCHAR *prop, *ret;
DWORD len = 0;
UINT r;
if (!(prop = msi_alloc( (str->len + 1) * sizeof(WCHAR) ))) return NULL;
lstrcpynW( prop, get_formstr_data(format, str), str->len + 1 );
r = msi_get_property( format->package->db, prop, NULL, &len );
if (r != ERROR_SUCCESS && r != ERROR_MORE_DATA)
{
msi_free( prop );
return NULL;
}
len++;
if ((ret = msi_alloc( len * sizeof(WCHAR) )))
msi_get_property( format->package->db, prop, ret, &len );
msi_free( prop );
*ret_len = len;
return ret;
}
static WCHAR *deformat_component( FORMAT *format, FORMSTR *str, int *ret_len )
{
WCHAR *key, *ret;
MSICOMPONENT *comp;
if (!(key = msi_alloc( (str->len + 1) * sizeof(WCHAR) ))) return NULL;
lstrcpynW(key, get_formstr_data(format, str), str->len + 1);
if (!(comp = msi_get_loaded_component( format->package, key )))
{
msi_free( key );
return NULL;
}
if (comp->Action == INSTALLSTATE_SOURCE)
ret = msi_resolve_source_folder( format->package, comp->Directory, NULL );
else
ret = strdupW( msi_get_target_folder( format->package, comp->Directory ) );
if (ret) *ret_len = strlenW( ret );
else *ret_len = 0;
msi_free( key );
return ret;
}
static WCHAR *deformat_file( FORMAT *format, FORMSTR *str, BOOL shortname, int *ret_len )
{
WCHAR *key, *ret = NULL;
const MSIFILE *file;
DWORD len = 0;
if (!(key = msi_alloc( (str->len + 1) * sizeof(WCHAR) ))) return NULL;
lstrcpynW(key, get_formstr_data(format, str), str->len + 1);
if (!(file = msi_get_loaded_file( format->package, key ))) goto done;
if (!shortname)
{
if ((ret = strdupW( file->TargetPath ))) len = strlenW( ret );
goto done;
}
if ((len = GetShortPathNameW(file->TargetPath, NULL, 0)) <= 0)
{
if ((ret = strdupW( file->TargetPath ))) len = strlenW( ret );
goto done;
}
len++;
if ((ret = msi_alloc( len * sizeof(WCHAR) )))
len = GetShortPathNameW( file->TargetPath, ret, len );
done:
msi_free( key );
*ret_len = len;
return ret;
}
static WCHAR *deformat_environment( FORMAT *format, FORMSTR *str, int *ret_len )
{
WCHAR *key, *ret = NULL;
DWORD len;
if (!(key = msi_alloc((str->len + 1) * sizeof(WCHAR)))) return NULL;
lstrcpynW(key, get_formstr_data(format, str), str->len + 1);
if ((len = GetEnvironmentVariableW( key, NULL, 0 )))
{
len++;
if ((ret = msi_alloc( len * sizeof(WCHAR) )))
*ret_len = GetEnvironmentVariableW( key, ret, len );
}
msi_free( key );
return ret;
}
static WCHAR *deformat_literal( FORMAT *format, FORMSTR *str, BOOL *propfound,
int *type, int *len )
{
LPCWSTR data = get_formstr_data(format, str);
WCHAR *replaced = NULL;
char ch = data[0];
if (ch == '\\')
{
str->n++;
if (str->len == 1)
{
str->len = 0;
replaced = NULL;
}
else
{
str->len = 1;
replaced = dup_formstr( format, str, len );
}
}
else if (ch == '~')
{
if (str->len != 1)
replaced = NULL;
else if ((replaced = msi_alloc( sizeof(WCHAR) )))
{
*replaced = 0;
*len = 0;
}
}
else if (ch == '%' || ch == '#' || ch == '!' || ch == '$')
{
str->n++;
str->len--;
switch (ch)
{
case '%':
replaced = deformat_environment( format, str, len ); break;
case '#':
replaced = deformat_file( format, str, FALSE, len ); break;
case '!':
replaced = deformat_file( format, str, TRUE, len ); break;
case '$':
replaced = deformat_component( format, str, len ); break;
}
*type = FORMAT_LITERAL;
}
else
{
replaced = deformat_property( format, str, len );
*type = FORMAT_LITERAL;
if (replaced)
*propfound = TRUE;
else
format->propfailed = TRUE;
}
return replaced;
}
static WCHAR *build_default_format( const MSIRECORD *record )
{
static const WCHAR fmt[] = {'%','i',':',' ','[','%','i',']',' ',0};
int i, count = MSI_RecordGetFieldCount( record );
WCHAR *ret, *tmp, buf[26];
DWORD size = 1;
if (!(ret = msi_alloc( sizeof(*ret) ))) return NULL;
ret[0] = 0;
for (i = 1; i <= count; i++)
{
size += sprintfW( buf, fmt, i, i );
if (!(tmp = msi_realloc( ret, size * sizeof(*ret) )))
{
msi_free( ret );
return NULL;
}
ret = tmp;
strcatW( ret, buf );
}
return ret;
}
static BOOL format_is_number(WCHAR x)
{
return ((x >= '0') && (x <= '9'));
}
static BOOL format_str_is_number(LPWSTR str)
{
LPWSTR ptr;
for (ptr = str; *ptr; ptr++)
if (!format_is_number(*ptr))
return FALSE;
return TRUE;
}
static BOOL format_is_alpha(WCHAR x)
{
return (!format_is_number(x) && x != '\0' &&
x != '[' && x != ']' && x != '{' && x != '}');
}
static BOOL format_is_literal(WCHAR x)
{
return (format_is_alpha(x) || format_is_number(x));
}
static int format_lex(FORMAT *format, FORMSTR **out)
{
int type, len = 1;
FORMSTR *str;
LPCWSTR data;
WCHAR ch;
*out = NULL;
if (!format->deformatted)
return FORMAT_NULL;
*out = msi_alloc_zero(sizeof(FORMSTR));
if (!*out)
return FORMAT_FAIL;
str = *out;
str->n = format->n;
str->len = 1;
data = get_formstr_data(format, str);
ch = data[0];
switch (ch)
{
case '{': type = FORMAT_LBRACE; break;
case '}': type = FORMAT_RBRACE; break;
case '[': type = FORMAT_LBRACK; break;
case ']': type = FORMAT_RBRACK; break;
case '~': type = FORMAT_PROPNULL; break;
case '\0': type = FORMAT_NULL; break;
default:
type = 0;
}
if (type)
{
str->type = type;
format->n++;
return type;
}
if (ch == '\\')
{
while (data[len] && data[len] != ']')
len++;
type = FORMAT_ESCAPE;
}
else if (format_is_alpha(ch))
{
while (format_is_literal(data[len]))
len++;
type = FORMAT_LITERAL;
}
else if (format_is_number(ch))
{
while (format_is_number(data[len]))
len++;
type = FORMAT_NUMBER;
if (data[len] != ']')
{
while (format_is_literal(data[len]))
len++;
type = FORMAT_LITERAL;
}
}
else
{
ERR("Got unknown character %c(%x)\n", ch, ch);
return FORMAT_ERROR;
}
format->n += len;
str->len = len;
str->type = type;
return type;
}
static FORMSTR *format_replace( FORMAT *format, BOOL propfound, BOOL nonprop,
int oldsize, int type, WCHAR *replace, int len )
{
FORMSTR *ret;
LPWSTR str, ptr;
DWORD size = 0;
int n;
if (replace)
{
if (!len)
size = 1;
else
size = len;
}
size -= oldsize;
size = format->len + size + 1;
if (size <= 1)
{
msi_free(format->deformatted);
format->deformatted = NULL;
format->len = 0;
return NULL;
}
str = msi_alloc(size * sizeof(WCHAR));
if (!str)
return NULL;
str[0] = '\0';
memcpy(str, format->deformatted, format->n * sizeof(WCHAR));
n = format->n;
if (replace)
{
if (!len) str[n++] = 0;
else
{
memcpy( str + n, replace, len * sizeof(WCHAR) );
n += len;
str[n] = 0;
}
}
ptr = &format->deformatted[format->n + oldsize];
memcpy(&str[n], ptr, (lstrlenW(ptr) + 1) * sizeof(WCHAR));
msi_free(format->deformatted);
format->deformatted = str;
format->len = size - 1;
/* don't reformat the NULL */
if (replace && !len)
format->n++;
if (!replace)
return NULL;
ret = msi_alloc_zero(sizeof(FORMSTR));
if (!ret)
return NULL;
ret->len = len;
ret->type = type;
ret->n = format->n;
ret->propfound = propfound;
ret->nonprop = nonprop;
return ret;
}
static WCHAR *replace_stack_group( FORMAT *format, STACK *values,
BOOL *propfound, BOOL *nonprop,
int *oldsize, int *type, int *len )
{
WCHAR *replaced;
FORMSTR *content, *node;
int n;
*nonprop = FALSE;
*propfound = FALSE;
node = stack_pop(values);
n = node->n;
*oldsize = node->len;
msi_free(node);
while ((node = stack_pop(values)))
{
*oldsize += node->len;
if (node->nonprop)
*nonprop = TRUE;
if (node->propfound)
*propfound = TRUE;
msi_free(node);
}
content = msi_alloc_zero(sizeof(FORMSTR));
content->n = n;
content->len = *oldsize;
content->type = FORMAT_LITERAL;
if (!format->groupfailed && (*oldsize == 2 ||
(format->propfailed && !*nonprop)))
{
msi_free(content);
return NULL;
}
else if (format->deformatted[content->n + 1] == '{' &&
format->deformatted[content->n + content->len - 2] == '}')
{
format->groupfailed = FALSE;
content->len = 0;
}
else if (*propfound && !*nonprop &&
!format->groupfailed && format->groups == 0)
{
content->n++;
content->len -= 2;
}
else
{
if (format->groups != 0)
format->groupfailed = TRUE;
*nonprop = TRUE;
}
replaced = dup_formstr( format, content, len );
*type = content->type;
msi_free(content);
if (format->groups == 0)
format->propfailed = FALSE;
return replaced;
}
static WCHAR *replace_stack_prop( FORMAT *format, STACK *values,
BOOL *propfound, BOOL *nonprop,
int *oldsize, int *type, int *len )
{
WCHAR *replaced;
FORMSTR *content, *node;
int n;
*propfound = FALSE;
*nonprop = FALSE;
node = stack_pop(values);
n = node->n;
*oldsize = node->len;
*type = stack_peek(values)->type;
msi_free(node);
while ((node = stack_pop(values)))
{
*oldsize += node->len;
if (*type != FORMAT_ESCAPE &&
stack_peek(values) && node->type != *type)
*type = FORMAT_LITERAL;
msi_free(node);
}
content = msi_alloc_zero(sizeof(FORMSTR));
content->n = n + 1;
content->len = *oldsize - 2;
content->type = *type;
if (*type == FORMAT_NUMBER && format->record)
{
replaced = deformat_index( format, content, len );
if (replaced)
*propfound = TRUE;
else
format->propfailed = TRUE;
if (replaced)
*type = format_str_is_number(replaced) ?
FORMAT_NUMBER : FORMAT_LITERAL;
}
else if (format->package)
{
replaced = deformat_literal( format, content, propfound, type, len );
}
else
{
*nonprop = TRUE;
content->n--;
content->len += 2;
replaced = dup_formstr( format, content, len );
}
msi_free(content);
return replaced;
}
static UINT replace_stack(FORMAT *format, STACK *stack, STACK *values)
{
WCHAR *replaced = NULL;
FORMSTR *beg, *top, *node;
BOOL propfound = FALSE, nonprop = FALSE, group = FALSE;
int type, n, len = 0, oldsize = 0;
node = stack_peek(values);
type = node->type;
n = node->n;
if (type == FORMAT_LBRACK)
replaced = replace_stack_prop( format, values, &propfound,
&nonprop, &oldsize, &type, &len );
else if (type == FORMAT_LBRACE)
{
replaced = replace_stack_group( format, values, &propfound,
&nonprop, &oldsize, &type, &len );
group = TRUE;
}
format->n = n;
beg = format_replace( format, propfound, nonprop, oldsize, type, replaced, len );
msi_free(replaced);
if (!beg)
return ERROR_SUCCESS;
format->n = beg->n + beg->len;
top = stack_peek(stack);
if (top)
{
type = top->type;
if ((type == FORMAT_LITERAL || type == FORMAT_NUMBER) &&
type == beg->type)
{
top->len += beg->len;
if (group)
top->nonprop = FALSE;
if (type == FORMAT_LITERAL)
top->nonprop = beg->nonprop;
if (beg->propfound)
top->propfound = TRUE;
msi_free(beg);
return ERROR_SUCCESS;
}
}
stack_push(stack, beg);
return ERROR_SUCCESS;
}
static BOOL verify_format(LPWSTR data)
{
int count = 0;
while (*data)
{
if (*data == '[' && *(data - 1) != '\\')
count++;
else if (*data == ']')
count--;
data++;
}
if (count > 0)
return FALSE;
return TRUE;
}
static DWORD deformat_string_internal(MSIPACKAGE *package, LPCWSTR ptr,
WCHAR** data, DWORD *len,
MSIRECORD* record)
{
FORMAT format;
FORMSTR *str = NULL;
STACK *stack, *temp;
FORMSTR *node;
int type;
if (!ptr)
{
*data = NULL;
*len = 0;
return ERROR_SUCCESS;
}
*data = strdupW(ptr);
*len = lstrlenW(ptr);
ZeroMemory(&format, sizeof(FORMAT));
format.package = package;
format.record = record;
format.deformatted = *data;
format.len = *len;
if (!verify_format(*data))
return ERROR_SUCCESS;
stack = create_stack();
temp = create_stack();
while ((type = format_lex(&format, &str)) != FORMAT_NULL)
{
if (type == FORMAT_LBRACK || type == FORMAT_LBRACE ||
type == FORMAT_LITERAL || type == FORMAT_NUMBER ||
type == FORMAT_ESCAPE || type == FORMAT_PROPNULL)
{
if (type == FORMAT_LBRACE)
{
format.propfailed = FALSE;
format.groups++;
}
else if (type == FORMAT_ESCAPE &&
!stack_find(stack, FORMAT_LBRACK))
{
format.n -= str->len - 1;
str->len = 1;
}
stack_push(stack, str);
}
else if (type == FORMAT_RBRACK || type == FORMAT_RBRACE)
{
if (type == FORMAT_RBRACE)
format.groups--;
stack_push(stack, str);
if (stack_find(stack, left_type(type)))
{
do
{
node = stack_pop(stack);
stack_push(temp, node);
} while (node->type != left_type(type));
replace_stack(&format, stack, temp);
}
}
}
*data = format.deformatted;
*len = format.len;
msi_free(str);
free_stack(stack);
free_stack(temp);
return ERROR_SUCCESS;
}
UINT MSI_FormatRecordW( MSIPACKAGE* package, MSIRECORD* record, LPWSTR buffer,
LPDWORD size )
{
WCHAR *format, *deformated = NULL;
UINT rc = ERROR_INVALID_PARAMETER;
DWORD len;
MSIRECORD *record_deformated;
int field_count, i;
TRACE("%p %p %p %p\n", package, record, buffer, size);
dump_record(record);
if (!(format = msi_dup_record_field( record, 0 )))
format = build_default_format( record );
field_count = MSI_RecordGetFieldCount(record);
record_deformated = MSI_CloneRecord(record);
if (!record_deformated)
{
rc = ERROR_OUTOFMEMORY;
goto end;
}
MSI_RecordSetStringW(record_deformated, 0, format);
for (i = 1; i <= field_count; i++)
{
if (MSI_RecordGetString(record, i))
{
deformat_string_internal(package, MSI_RecordGetString(record, i), &deformated, &len, NULL);
MSI_RecordSetStringW(record_deformated, i, deformated);
msi_free(deformated);
}
}
deformat_string_internal(package, format, &deformated, &len, record_deformated);
if (buffer)
{
if (*size>len)
{
memcpy(buffer,deformated,len*sizeof(WCHAR));
rc = ERROR_SUCCESS;
buffer[len] = 0;
}
else
{
if (*size > 0)
{
memcpy(buffer,deformated,(*size)*sizeof(WCHAR));
buffer[(*size)-1] = 0;
}
rc = ERROR_MORE_DATA;
}
}
else rc = ERROR_SUCCESS;
*size = len;
msiobj_release(&record_deformated->hdr);
end:
msi_free( format );
msi_free( deformated );
return rc;
}
UINT WINAPI MsiFormatRecordW( MSIHANDLE hInstall, MSIHANDLE hRecord,
LPWSTR szResult, LPDWORD sz )
{
UINT r = ERROR_INVALID_HANDLE;
MSIPACKAGE *package;
MSIRECORD *record;
TRACE("%d %d %p %p\n", hInstall, hRecord, szResult, sz);
package = msihandle2msiinfo( hInstall, MSIHANDLETYPE_PACKAGE );
if (!package)
{
HRESULT hr;
IWineMsiRemotePackage *remote_package;
BSTR value = NULL;
awstring wstr;
remote_package = (IWineMsiRemotePackage *)msi_get_remote( hInstall );
if (remote_package)
{
hr = IWineMsiRemotePackage_FormatRecord( remote_package, hRecord,
&value );
if (FAILED(hr))
goto done;
wstr.unicode = TRUE;
wstr.str.w = szResult;
r = msi_strcpy_to_awstring( value, SysStringLen(value), &wstr, sz );
done:
IWineMsiRemotePackage_Release( remote_package );
SysFreeString( value );
if (FAILED(hr))
{
if (HRESULT_FACILITY(hr) == FACILITY_WIN32)
return HRESULT_CODE(hr);
return ERROR_FUNCTION_FAILED;
}
return r;
}
}
record = msihandle2msiinfo( hRecord, MSIHANDLETYPE_RECORD );
if (!record)
return ERROR_INVALID_HANDLE;
if (!sz)
{
msiobj_release( &record->hdr );
if (szResult)
return ERROR_INVALID_PARAMETER;
else
return ERROR_SUCCESS;
}
r = MSI_FormatRecordW( package, record, szResult, sz );
msiobj_release( &record->hdr );
if (package)
msiobj_release( &package->hdr );
return r;
}
UINT WINAPI MsiFormatRecordA( MSIHANDLE hInstall, MSIHANDLE hRecord,
LPSTR szResult, LPDWORD sz )
{
UINT r;
DWORD len, save;
LPWSTR value;
TRACE("%d %d %p %p\n", hInstall, hRecord, szResult, sz);
if (!hRecord)
return ERROR_INVALID_HANDLE;
if (!sz)
{
if (szResult)
return ERROR_INVALID_PARAMETER;
else
return ERROR_SUCCESS;
}
r = MsiFormatRecordW( hInstall, hRecord, NULL, &len );
if (r != ERROR_SUCCESS)
return r;
value = msi_alloc(++len * sizeof(WCHAR));
if (!value)
return ERROR_OUTOFMEMORY;
r = MsiFormatRecordW( hInstall, hRecord, value, &len );
if (r != ERROR_SUCCESS)
goto done;
save = len + 1;
len = WideCharToMultiByte(CP_ACP, 0, value, len + 1, NULL, 0, NULL, NULL);
WideCharToMultiByte(CP_ACP, 0, value, len, szResult, *sz, NULL, NULL);
if (szResult && len > *sz)
{
if (*sz) szResult[*sz - 1] = '\0';
r = ERROR_MORE_DATA;
}
*sz = save - 1;
done:
msi_free(value);
return r;
}
/* wrapper to resist a need for a full rewrite right now */
DWORD deformat_string( MSIPACKAGE *package, const WCHAR *fmt, WCHAR **data )
{
DWORD len;
MSIRECORD *rec;
*data = NULL;
if (!fmt) return 0;
if (!(rec = MSI_CreateRecord( 1 ))) return 0;
MSI_RecordSetStringW( rec, 0, fmt );
MSI_FormatRecordW( package, rec, NULL, &len );
if (!(*data = msi_alloc( ++len * sizeof(WCHAR) )))
{
msiobj_release( &rec->hdr );
return 0;
}
MSI_FormatRecordW( package, rec, *data, &len );
msiobj_release( &rec->hdr );
return len;
}