diff options
Diffstat (limited to 'source3')
-rw-r--r-- | source3/rpc_client/cli_srvsvc.c | 57 | ||||
-rw-r--r-- | source3/rpcclient/cmd_srvsvc.c | 5 | ||||
-rw-r--r-- | source3/utils/net_rpc.c | 456 |
3 files changed, 396 insertions, 122 deletions
diff --git a/source3/rpc_client/cli_srvsvc.c b/source3/rpc_client/cli_srvsvc.c index a8a6bc1ff6..b2449a7903 100644 --- a/source3/rpc_client/cli_srvsvc.c +++ b/source3/rpc_client/cli_srvsvc.c @@ -267,6 +267,63 @@ WERROR cli_srvsvc_net_share_get_info(struct cli_state *cli, info->switch_value = info_level; switch(info_level) { + case 1: + { + SRV_SHARE_INFO_1 *info1 = &info->share.info1; + SH_INFO_1_STR *info1_str = &info1->info_1_str; + + char *s; + + info->share.info1 = r.info.share.info1; + + /* Duplicate strings */ + + s = unistr2_tdup(mem_ctx, &info1_str->uni_netname); + if (s) + init_unistr2(&info1_str->uni_netname, + s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info1_str->uni_remark); + if (s) + init_unistr2(&info1_str->uni_remark, + s, UNI_STR_TERMINATE); + + break; + } + case 2: + { + SRV_SHARE_INFO_2 *info2 = &info->share.info2; + SH_INFO_2_STR *info2_str = &info2->info_2_str; + + char *s; + + info->share.info2 = r.info.share.info2; + + /* Duplicate strings */ + + s = unistr2_tdup(mem_ctx, &info2_str->uni_netname); + if (s) + init_unistr2(&info2_str->uni_netname, + s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info2_str->uni_remark); + if (s) + init_unistr2(&info2_str->uni_remark, + s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info2_str->uni_path); + if (s) + init_unistr2(&info2_str->uni_path, + s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info2_str->uni_passwd); + if (s) + init_unistr2(&info2_str->uni_passwd, + s, UNI_STR_TERMINATE); + + + break; + } case 502: { SRV_SHARE_INFO_502 *info502 = &info->share.info502; diff --git a/source3/rpcclient/cmd_srvsvc.c b/source3/rpcclient/cmd_srvsvc.c index bdb2f22b54..1d173ffdff 100644 --- a/source3/rpcclient/cmd_srvsvc.c +++ b/source3/rpcclient/cmd_srvsvc.c @@ -260,6 +260,11 @@ static void display_share_info_502(SRV_SHARE_INFO_502 *info502) printf("\tpath:\t%s\n", path); printf("\tpassword:\t%s\n", passwd); + printf("\ttype:\t0x%x\n", info502->info_502.type); + printf("\tperms:\t%d\n", info502->info_502.perms); + printf("\tmax_uses:\t%d\n", info502->info_502.max_uses); + printf("\tnum_uses:\t%d\n", info502->info_502.num_uses); + if (info502->info_502_str.sd) display_sec_desc(info502->info_502_str.sd); diff --git a/source3/utils/net_rpc.c b/source3/utils/net_rpc.c index 614f19c803..fb34b1e9e3 100644 --- a/source3/utils/net_rpc.c +++ b/source3/utils/net_rpc.c @@ -708,7 +708,7 @@ static NTSTATUS rpc_user_rename_internals(const DOM_SID *domain_sid, const char SAM_USER_INFO_7 info7; if (argc != 2) { - d_printf("New and old username must be specified\n"); + d_printf("Old and new username must be specified\n"); rpc_user_usage(argc, argv); return NT_STATUS_OK; } @@ -1151,12 +1151,9 @@ int net_rpc_user(int argc, const char **argv) }; if (argc == 0) { - if (opt_long_list_entries) { - } else { - } - return run_rpc_command(NULL,PI_SAMR, 0, - rpc_user_list_internals, - argc, argv); + return run_rpc_command(NULL,PI_SAMR, 0, + rpc_user_list_internals, + argc, argv); } return net_run_function(argc, argv, func, rpc_user_usage); @@ -2436,9 +2433,6 @@ int net_rpc_group(int argc, const char **argv) }; if (argc == 0) { - if (opt_long_list_entries) { - } else { - } return run_rpc_command(NULL, PI_SAMR, 0, rpc_group_list_internals, argc, argv); @@ -2477,7 +2471,7 @@ rpc_share_add_internals(const DOM_SID *domain_sid, const char *domain_name, WERROR result; char *sharename=talloc_strdup(mem_ctx, argv[0]); char *path; - uint32 type=0; /* only allow disk shares to be added */ + uint32 type = STYPE_DISKTREE; /* only allow disk shares to be added */ uint32 num_users=0, perms=0; char *password=NULL; /* don't allow a share password */ uint32 level = 2; @@ -2574,6 +2568,139 @@ static void display_share_info_1(SRV_SHARE_INFO_1 *info1) } + +static WERROR get_share_info(struct cli_state *cli, TALLOC_CTX *mem_ctx, + uint32 level, int argc, const char **argv, + SRV_SHARE_INFO_CTR *ctr) +{ + WERROR result; + SRV_SHARE_INFO info; + + /* no specific share requested, enumerate all */ + if (argc == 0) { + + ENUM_HND hnd; + uint32 preferred_len = 0xffffffff; + + init_enum_hnd(&hnd, 0); + + result = cli_srvsvc_net_share_enum(cli, mem_ctx, level, ctr, + preferred_len, &hnd); + goto done; + } + + /* request just one share */ + result = cli_srvsvc_net_share_get_info(cli, mem_ctx, argv[0], level, &info); + + if (!W_ERROR_IS_OK(result)) + goto done; + + /* construct ctr */ + ZERO_STRUCTP(ctr); + + ctr->info_level = ctr->switch_value = level; + ctr->ptr_share_info = ctr->ptr_entries = 1; + ctr->num_entries = ctr->num_entries2 = 1; + + switch (level) { + case 1: + { + char *s; + SRV_SHARE_INFO_1 *info1; + + ctr->share.info1 = TALLOC_ARRAY(mem_ctx, SRV_SHARE_INFO_1, 1); + info1 = ctr->share.info1; + + memset(ctr->share.info1, 0, sizeof(SRV_SHARE_INFO_1)); + + /* Copy pointer crap */ + + memcpy(&info1->info_1, &info.share.info1.info_1, sizeof(SH_INFO_1)); + + /* Duplicate strings */ + + s = unistr2_tdup(mem_ctx, &info.share.info1.info_1_str.uni_netname); + if (s) + init_unistr2(&info1->info_1_str.uni_netname, s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info.share.info1.info_1_str.uni_remark); + if (s) + init_unistr2(&info1->info_1_str.uni_remark, s, UNI_STR_TERMINATE); + } + case 2: + { + char *s; + SRV_SHARE_INFO_2 *info2; + + ctr->share.info2 = TALLOC_ARRAY(mem_ctx, SRV_SHARE_INFO_2, 1); + info2 = ctr->share.info2; + + memset(ctr->share.info2, 0, sizeof(SRV_SHARE_INFO_2)); + + /* Copy pointer crap */ + + memcpy(&info2->info_2, &info.share.info2.info_2, sizeof(SH_INFO_2)); + + /* Duplicate strings */ + + s = unistr2_tdup(mem_ctx, &info.share.info2.info_2_str.uni_netname); + if (s) + init_unistr2(&info2->info_2_str.uni_netname, s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info.share.info2.info_2_str.uni_remark); + if (s) + init_unistr2(&info2->info_2_str.uni_remark, s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info.share.info2.info_2_str.uni_path); + if (s) + init_unistr2(&info2->info_2_str.uni_path, s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info.share.info2.info_2_str.uni_passwd); + if (s) + init_unistr2(&info2->info_2_str.uni_passwd, s, UNI_STR_TERMINATE); + } + case 502: + { + char *s; + SRV_SHARE_INFO_502 *info502; + + ctr->share.info502 = TALLOC_ARRAY(mem_ctx, SRV_SHARE_INFO_502, 1); + info502 = ctr->share.info502; + + memset(ctr->share.info502, 0, sizeof(SRV_SHARE_INFO_502)); + + /* Copy pointer crap */ + + memcpy(&info502->info_502, &info.share.info502.info_502, sizeof(SH_INFO_502)); + + /* Duplicate strings */ + + s = unistr2_tdup(mem_ctx, &info.share.info502.info_502_str.uni_netname); + if (s) + init_unistr2(&info502->info_502_str.uni_netname, s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info.share.info502.info_502_str.uni_remark); + if (s) + init_unistr2(&info502->info_502_str.uni_remark, s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info.share.info502.info_502_str.uni_path); + if (s) + init_unistr2(&info502->info_502_str.uni_path, s, UNI_STR_TERMINATE); + + s = unistr2_tdup(mem_ctx, &info.share.info502.info_502_str.uni_passwd); + if (s) + init_unistr2(&info502->info_502_str.uni_passwd, s, UNI_STR_TERMINATE); + + info502->info_502_str.sd = dup_sec_desc(mem_ctx, info.share.info502.info_502_str.sd); + + } + + } /* switch */ + +done: + return result; +} + /** * List shares on a remote RPC server * @@ -2597,14 +2724,9 @@ rpc_share_list_internals(const DOM_SID *domain_sid, const char *domain_name, { SRV_SHARE_INFO_CTR ctr; WERROR result; - ENUM_HND hnd; - uint32 preferred_len = 0xffffffff, i; - - init_enum_hnd(&hnd, 0); - - result = cli_srvsvc_net_share_enum( - cli, mem_ctx, 1, &ctr, preferred_len, &hnd); + uint32 i, level = 1; + result = get_share_info(cli, mem_ctx, level, argc, argv, &ctr); if (!W_ERROR_IS_OK(result)) goto done; @@ -2622,6 +2744,52 @@ rpc_share_list_internals(const DOM_SID *domain_sid, const char *domain_name, return W_ERROR_IS_OK(result) ? NT_STATUS_OK : NT_STATUS_UNSUCCESSFUL; } +/*** + * 'net rpc share list' entrypoint. + * @param argc Standard main() style argc + * @param argv Standard main() style argv. Initial components are already + * stripped + **/ +static int rpc_share_list(int argc, const char **argv) +{ + return run_rpc_command(NULL, PI_SRVSVC, 0, rpc_share_list_internals, argc, argv); +} + +static BOOL check_share_availability(struct cli_state *cli, const char *netname) +{ + if (!cli_send_tconX(cli, netname, "A:", "", 0)) { + d_printf("skipping [%s]: not a file share.\n", netname); + return False; + } + + if (!cli_tdis(cli)) + return False; + + return True; +} + +static BOOL check_share_sanity(struct cli_state *cli, fstring netname, uint32 type) +{ + /* only support disk shares */ + if (! ( type == STYPE_DISKTREE || type == (STYPE_DISKTREE | STYPE_HIDDEN)) ) { + printf("share [%s] is not a diskshare (type: %x)\n", netname, type); + return False; + } + + /* skip builtin shares */ + /* FIXME: should print$ be added too ? */ + if (strequal(netname,"IPC$") || strequal(netname,"ADMIN$") || + strequal(netname,"global")) + return False; + + if (opt_exclude && in_list(netname, opt_exclude, False)) { + printf("excluding [%s]\n", netname); + return False; + } + + return check_share_availability(cli, netname); +} + /** * Migrate shares from a remote RPC server to the local RPC srever * @@ -2645,24 +2813,18 @@ rpc_share_migrate_shares_internals(const DOM_SID *domain_sid, const char *domain WERROR result; NTSTATUS nt_status = NT_STATUS_UNSUCCESSFUL; SRV_SHARE_INFO_CTR ctr_src; - ENUM_HND hnd; - uint32 type = 0; /* only allow disk shares to be added */ - uint32 num_uses = 0, perms = 0, max_uses = 0; + uint32 type = STYPE_DISKTREE; /* only allow disk shares to be added */ char *password = NULL; /* don't allow a share password */ - uint32 preferred_len = 0xffffffff, i; + uint32 i; BOOL got_dst_srvsvc_pipe = False; struct cli_state *cli_dst = NULL; uint32 level = 502; /* includes secdesc */ - SEC_DESC *share_sd = NULL; - init_enum_hnd(&hnd, 0); - - result = cli_srvsvc_net_share_enum( - cli, mem_ctx, level, &ctr_src, preferred_len, &hnd); + result = get_share_info(cli, mem_ctx, level, argc, argv, &ctr_src); if (!W_ERROR_IS_OK(result)) goto done; - /* connect local PI_SRVSVC */ + /* connect destination PI_SRVSVC */ nt_status = connect_pipe(&cli_dst, PI_SRVSVC, &got_dst_srvsvc_pipe); if (!NT_STATUS_IS_OK(nt_status)) return nt_status; @@ -2680,58 +2842,24 @@ rpc_share_migrate_shares_internals(const DOM_SID *domain_sid, const char *domain remark, &ctr_src.share.info502[i].info_502_str.uni_remark); rpcstr_pull_unistr2_fstring( path, &ctr_src.share.info502[i].info_502_str.uni_path); - num_uses = ctr_src.share.info502[i].info_502.num_uses; - max_uses = ctr_src.share.info502[i].info_502.max_uses; - perms = ctr_src.share.info502[i].info_502.perms; - - - if (opt_acls) - share_sd = dup_sec_desc( - mem_ctx, ctr_src.share.info502[i].info_502_str.sd); - /* since we do not have NetShareGetInfo implemented in samba3 we - only can skip inside the enum-ctr_src */ - if (argc == 1) { - char *one_share = talloc_strdup(mem_ctx, argv[0]); - if (!strequal(netname, one_share)) - continue; - } - - /* skip builtin shares */ - /* FIXME: should print$ be added too ? */ - if (strequal(netname,"IPC$") || strequal(netname,"ADMIN$") || - strequal(netname,"global")) - continue; - - if (opt_exclude && in_list(netname, opt_exclude, False)) { - printf("excluding [%s]\n", netname); + if (!check_share_sanity(cli, netname, ctr_src.share.info502[i].info_502.type)) continue; - } - /* only work with file-shares */ - if (!cli_send_tconX(cli, netname, "A:", "", 0)) { - d_printf("skipping [%s]: not a file share.\n", netname); - continue; - } - - if (!cli_tdis(cli)) - goto done; - - - /* finallly add the share on the dst server - please note that samba currently does not allow to - add a share without existing directory */ + /* finally add the share on the dst server */ printf("migrating: [%s], path: %s, comment: %s, %s share-ACLs\n", netname, path, remark, opt_acls ? "including" : "without" ); if (opt_verbose && opt_acls) - display_sec_desc(share_sd); - - result = cli_srvsvc_net_share_add(cli_dst, mem_ctx, netname, type, - remark, perms, max_uses, - num_uses, path, password, - level, share_sd); + display_sec_desc(ctr_src.share.info502[i].info_502_str.sd); + + result = cli_srvsvc_net_share_add(cli_dst, mem_ctx, netname, type, remark, + ctr_src.share.info502[i].info_502.perms, + ctr_src.share.info502[i].info_502.max_uses, + ctr_src.share.info502[i].info_502.num_uses, + path, password, level, + opt_acls? ctr_src.share.info502[i].info_502_str.sd : NULL); if (W_ERROR_V(result) == W_ERROR_V(WERR_ALREADY_EXISTS)) { printf(" [%s] does already exist\n", netname); @@ -2962,13 +3090,12 @@ rpc_share_migrate_files_internals(const DOM_SID *domain_sid, const char *domain_ WERROR result; NTSTATUS nt_status = NT_STATUS_UNSUCCESSFUL; SRV_SHARE_INFO_CTR ctr_src; - ENUM_HND hnd; - uint32 preferred_len = 0xffffffff, i; - uint32 level = 2; + uint32 i; + uint32 level = 502; struct copy_clistate cp_clistate; BOOL got_src_share = False; BOOL got_dst_share = False; - pstring mask; + pstring mask = "\\*"; char *dst = NULL; /* decrese argc and safe mode */ @@ -2976,57 +3103,27 @@ rpc_share_migrate_files_internals(const DOM_SID *domain_sid, const char *domain_ dst = SMB_STRDUP(opt_destination?opt_destination:"127.0.0.1"); - init_enum_hnd(&hnd, 0); - - result = cli_srvsvc_net_share_enum( - cli, mem_ctx, level, &ctr_src, preferred_len, &hnd); + result = get_share_info(cli, mem_ctx, level, argc, argv, &ctr_src); if (!W_ERROR_IS_OK(result)) goto done; for (i = 0; i < ctr_src.num_entries; i++) { - fstring netname = "", remark = "", path = ""; + fstring netname = ""; rpcstr_pull_unistr2_fstring( - netname, &ctr_src.share.info2[i].info_2_str.uni_netname); - rpcstr_pull_unistr2_fstring( - remark, &ctr_src.share.info2[i].info_2_str.uni_remark); - rpcstr_pull_unistr2_fstring( - path, &ctr_src.share.info2[i].info_2_str.uni_path); - - /* since we do not have NetShareGetInfo implemented in samba3 we - only can skip inside the enum-ctr_src */ - if (argc == 1) { - char *one_share = talloc_strdup(mem_ctx, argv[0]); - if (!strequal(netname, one_share)) - continue; - } + netname, &ctr_src.share.info502[i].info_502_str.uni_netname); - /* skip builtin and hidden shares - In particular, one might not want to mirror whole discs :) */ - if (strequal(netname,"IPC$") || strequal(netname,"ADMIN$")) + if (!check_share_sanity(cli, netname, ctr_src.share.info502[i].info_502.type)) continue; - + + /* one might not want to mirror whole discs :) */ if (strequal(netname, "print$") || netname[1] == '$') { d_printf("skipping [%s]: builtin/hidden share\n", netname); continue; } - if (opt_exclude && in_list(netname, opt_exclude, False)) { - printf("excluding [%s]\n", netname); - continue; - } - - /* only work with file-shares */ - if (!cli_send_tconX(cli, netname, "A:", "", 0)) { - d_printf("skipping [%s]: not a file share.\n", netname); - continue; - } - - if (!cli_tdis(cli)) - return NT_STATUS_UNSUCCESSFUL; - switch (cp_clistate.mode) { case NET_MODE_SHARE_MIGRATE: @@ -3073,10 +3170,6 @@ rpc_share_migrate_files_internals(const DOM_SID *domain_sid, const char *domain_ goto done; } - /* now call the filesync */ - pstrcpy(mask, "\\*"); - - if (!sync_files(&cp_clistate, mask)) { d_printf("could not handle files for share: %s\n", netname); nt_status = NT_STATUS_UNSUCCESSFUL; @@ -3112,6 +3205,121 @@ static int rpc_share_migrate_files(int argc, const char **argv) } /** + * Migrate share-ACLs from a remote RPC server to the local RPC srever + * + * All parameters are provided by the run_rpc_command function, except for + * argc, argv which are passes through. + * + * @param domain_sid The domain sid acquired from the remote server + * @param cli A cli_state connected to the server. + * @param mem_ctx Talloc context, destoyed on completion of the function. + * @param argc Standard main() style argc + * @param argv Standard main() style argv. Initial components are already + * stripped + * + * @return Normal NTSTATUS return. + **/ +static NTSTATUS +rpc_share_migrate_security_internals(const DOM_SID *domain_sid, const char *domain_name, + struct cli_state *cli, TALLOC_CTX *mem_ctx, + int argc, const char **argv) +{ + WERROR result; + NTSTATUS nt_status = NT_STATUS_UNSUCCESSFUL; + SRV_SHARE_INFO_CTR ctr_src; + SRV_SHARE_INFO info; + uint32 i; + BOOL got_dst_srvsvc_pipe = False; + struct cli_state *cli_dst = NULL; + uint32 level = 502; /* includes secdesc */ + + result = get_share_info(cli, mem_ctx, level, argc, argv, &ctr_src); + + if (!W_ERROR_IS_OK(result)) + goto done; + + /* connect destination PI_SRVSVC */ + nt_status = connect_pipe(&cli_dst, PI_SRVSVC, &got_dst_srvsvc_pipe); + if (!NT_STATUS_IS_OK(nt_status)) + return nt_status; + + + for (i = 0; i < ctr_src.num_entries; i++) { + + fstring netname = "", remark = "", path = ""; + /* reset error-code */ + nt_status = NT_STATUS_UNSUCCESSFUL; + + rpcstr_pull_unistr2_fstring( + netname, &ctr_src.share.info502[i].info_502_str.uni_netname); + rpcstr_pull_unistr2_fstring( + remark, &ctr_src.share.info502[i].info_502_str.uni_remark); + rpcstr_pull_unistr2_fstring( + path, &ctr_src.share.info502[i].info_502_str.uni_path); + + if (!check_share_sanity(cli, netname, ctr_src.share.info502[i].info_502.type)) + continue; + + printf("migrating: [%s], path: %s, comment: %s, including share-ACLs\n", + netname, path, remark); + + if (opt_verbose) + display_sec_desc(ctr_src.share.info502[i].info_502_str.sd); + + /* init info */ + ZERO_STRUCT(info); + + info.switch_value = level; + info.ptr_share_ctr = 1; + + /* FIXME: shouldn't we be able to just set the security descriptor ? */ + info.share.info502 = ctr_src.share.info502[i]; + + /* finally modify the share on the dst server */ + result = cli_srvsvc_net_share_set_info(cli_dst, mem_ctx, netname, level, &info); + + if (!W_ERROR_IS_OK(result)) { + printf("cannot set share-acl: %s\n", dos_errstr(result)); + goto done; + } + + } + + nt_status = NT_STATUS_OK; + +done: + if (got_dst_srvsvc_pipe) { + cli_nt_session_close(cli_dst); + cli_shutdown(cli_dst); + } + + return nt_status; + +} + +/** + * Migrate share-acls from a rpc-server to another + * + * @param argc Standard main() style argc + * @param argv Standard main() style argv. Initial components are already + * stripped + * + * @return A shell status integer (0 for success) + **/ +static int rpc_share_migrate_security(int argc, const char **argv) +{ + + if (!opt_host) { + printf("no server to migrate\n"); + return -1; + } + + return run_rpc_command(NULL, PI_SRVSVC, 0, + rpc_share_migrate_security_internals, + argc, argv); +} + +/** * Migrate shares (including share-definitions, share-acls and files with acls/attrs) * from one server to another * @@ -3131,15 +3339,18 @@ static int rpc_share_migrate_all(int argc, const char **argv) return -1; } + /* order is important. we don't want to be locked out by the share-acl + * before copying files - gd */ + ret = run_rpc_command(NULL, PI_SRVSVC, 0, rpc_share_migrate_shares_internals, argc, argv); if (ret) return ret; -#if 0 - ret = run_rpc_command(NULL, PI_SRVSVC, 0, rpc_share_migrate_shares_security_internals, argc, argv); + + ret = run_rpc_command(NULL, PI_SRVSVC, 0, rpc_share_migrate_files_internals, argc, argv); if (ret) return ret; -#endif - return run_rpc_command(NULL, PI_SRVSVC, 0, rpc_share_migrate_files_internals, argc, argv); + + return run_rpc_command(NULL, PI_SRVSVC, 0, rpc_share_migrate_security_internals, argc, argv); } @@ -3156,7 +3367,7 @@ static int rpc_share_migrate(int argc, const char **argv) {"all", rpc_share_migrate_all}, {"files", rpc_share_migrate_files}, {"help", rpc_share_usage}, -/* {"security", rpc_share_migrate_security},*/ + {"security", rpc_share_migrate_security}, {"shares", rpc_share_migrate_shares}, {NULL, NULL} }; @@ -3169,7 +3380,7 @@ static int rpc_share_migrate(int argc, const char **argv) struct full_alias { DOM_SID sid; - int num_members; + uint32 num_members; DOM_SID *members; }; @@ -3951,6 +4162,7 @@ int net_rpc_share(int argc, const char **argv) {"delete", rpc_share_delete}, {"allowedusers", rpc_share_allowedusers}, {"migrate", rpc_share_migrate}, + {"list", rpc_share_list}, {NULL, NULL} }; |