diff --git a/.github/workflows/test_fb3.yml b/.github/workflows/test_fb3.yml index ffd3265..16bfa05 100644 --- a/.github/workflows/test_fb3.yml +++ b/.github/workflows/test_fb3.yml @@ -23,6 +23,9 @@ jobs: sudo systemctl restart firebird3.0 sudo chmod 0664 /etc/firebird/3.0/SYSDBA.password grep '=' /etc/firebird/3.0/SYSDBA.password |sed 's/^/export /' >test_user.env + export FIREBIRD_LOG=/var/log/firebird/firebird3.0.log >> test_user.env + sudo touch /var/log/firebird/firebird3.0.log + sudo chmod 777 /var/log/firebird/firebird3.0.log - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v5 diff --git a/_examples/service_manager.go b/_examples/service_manager.go new file mode 100644 index 0000000..732d8a5 --- /dev/null +++ b/_examples/service_manager.go @@ -0,0 +1,149 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package main + +import ( + "database/sql" + "fmt" + "github.com/nakagami/firebirdsql" + "os" +) + +func main() { + // Create some test database + conn, err := sql.Open("firebirdsql_createdb", "sysdba:test@localhost/tmp/gofirebirdsqltest.fdb") + defer conn.Close() + if err != nil || conn == nil { + panic(err) + } + err = conn.Ping() + if err != nil { + panic(err) + } + // Base service manager tools + sm, err := firebirdsql.NewServiceManager("localhost:3050", "sysdba", "test", firebirdsql.NewServiceManagerOptions()) + defer sm.Close() + if err != nil { + panic(err) + } + // Extract some server info + version, err := sm.GetServerVersion() + if err != nil { + panic(err) + } + homeDir, err := sm.GetHomeDir() + if err != nil { + panic(err) + } + fmt.Printf("Server version: %s, installed in %s\n", version.Raw, homeDir) + + //Database statistics + dbStats, err := sm.GetDbStatsString("/tmp/gofirebirdsqltest.fdb", firebirdsql.NewStatisticsOptions(firebirdsql.WithOnlyHeaderPages())) + if err != nil { + panic(err) + } + fmt.Println(dbStats) + + // User management + um, err := firebirdsql.NewUserManager("localhost:3050", "sysdba", "test", firebirdsql.NewServiceManagerOptions(), firebirdsql.NewUserManagerOptions()) + defer um.Close() + if err != nil { + panic(err) + } + + // Create new user + _ = um.AddUser(firebirdsql.NewUser(firebirdsql.WithUsername("testuser"), firebirdsql.WithPassword("testpass"), firebirdsql.WithAdmin())) + // Obtain existing users + users, err := um.GetUsers() + if err != nil { + panic(err) + } + for _, user := range users { + fmt.Printf("User: %s, admin role: %v\n", *user.Username, *user.Admin) + } + // Drop user + _ = um.DeleteUser(firebirdsql.NewUser(firebirdsql.WithUsername("testuser"))) + + // Backup manager + + // Backup manager (gbak utility) + bm, err := firebirdsql.NewBackupManager("localhost:3050", "sysdba", "test", firebirdsql.NewServiceManagerOptions()) + // no need to close BackupManager, because it opens connection during Backup/Restore and closes it automatically + if err != nil { + panic(err) + } + + // Create backup with gbak using logging from server to client + fmt.Println("\ngback:\n") + done := make(chan bool) + resChan := make(chan string) + go func() { + err = bm.Backup("/tmp/gofirebirdsqltest.fdb", "/tmp/gofirebirdsqltest.fbk", firebirdsql.NewBackupOptions(), resChan) + if err != nil { + panic(err) + } + done <- true + }() + + cont := true + var s string + for cont { + select { + case s = <-resChan: + fmt.Println(s) + case <-done: + cont = false + break + } + } + + // NBackup manager (nbackup utility) + nbk, err := firebirdsql.NewNBackupManager("localhost:3050", "sysdba", "test", firebirdsql.NewServiceManagerOptions()) + // no need to close NBackupManager, because it opens connection during Backup/Restore and closes it automatically + if err != nil { + panic(err) + } + + // Create zero level backup with nbackup using logging from server to client + os.Remove("/tmp/gofirebirdsqltest.nbk") + fmt.Println("\nNBackup:\n") + go func() { + err = nbk.Backup("/tmp/gofirebirdsqltest.fdb", "/tmp/gofirebirdsqltest.nbk", firebirdsql.NewNBackupOptions(), resChan) + if err != nil { + panic(err) + } + done <- true + }() + + cont = true + for cont { + select { + case s = <-resChan: + fmt.Println(s) + case <-done: + cont = false + break + } + } +} diff --git a/backup_manager.go b/backup_manager.go new file mode 100644 index 0000000..a7bcc18 --- /dev/null +++ b/backup_manager.go @@ -0,0 +1,365 @@ +package firebirdsql + +type BackupManager struct { + connBuilder func() (*ServiceManager, error) +} + +type BackupOptions struct { + IgnoreChecksums bool + IgnoreLimboTransactions bool + MetadataOnly bool + GarbageCollect bool + Transportable bool + ConvertExternalTablesToInternalTables bool + Expand bool +} + +type BackupOption func(*BackupOptions) + +type RestoreOptions struct { + Replace bool + DeactivateIndexes bool + RestoreShadows bool + EnforceConstraints bool + CommitAfterEachTable bool + UseAllPageSpace bool + PageSize int32 + CacheBuffers int32 +} + +type RestoreOption func(*RestoreOptions) + +func GetDefaultBackupOptions() BackupOptions { + return BackupOptions{ + IgnoreChecksums: false, + IgnoreLimboTransactions: false, + MetadataOnly: false, + GarbageCollect: true, + Transportable: true, + ConvertExternalTablesToInternalTables: true, + Expand: false, + } +} + +func WithIgnoreChecksums() BackupOption { + return func(opts *BackupOptions) { + opts.IgnoreChecksums = true + } +} + +func WithoutIgnoreChecksums() BackupOption { + return func(opts *BackupOptions) { + opts.IgnoreChecksums = false + } +} + +func WithIgnoreLimboTransactions() BackupOption { + return func(opts *BackupOptions) { + opts.IgnoreLimboTransactions = true + } +} + +func WithoutIgnoreLimboTransactions() BackupOption { + return func(opts *BackupOptions) { + opts.IgnoreLimboTransactions = false + } +} + +func WithMetadataOnly() BackupOption { + return func(opts *BackupOptions) { + opts.MetadataOnly = true + } +} + +func WithoutMetadataOnly() BackupOption { + return func(opts *BackupOptions) { + opts.MetadataOnly = false + } +} + +func WithGarbageCollect() BackupOption { + return func(opts *BackupOptions) { + opts.GarbageCollect = true + } +} + +func WithoutGarbageCollect() BackupOption { + return func(opts *BackupOptions) { + opts.GarbageCollect = false + } +} + +func WithTransportable() BackupOption { + return func(opts *BackupOptions) { + opts.Transportable = true + } +} + +func WithoutTransportable() BackupOption { + return func(opts *BackupOptions) { + opts.Transportable = false + } +} + +func WithConvertExternalTablesToInternalTables() BackupOption { + return func(opts *BackupOptions) { + opts.ConvertExternalTablesToInternalTables = true + } +} + +func WithoutConvertExternalTablesToInternalTables() BackupOption { + return func(opts *BackupOptions) { + opts.ConvertExternalTablesToInternalTables = false + } +} + +func WithExpand() BackupOption { + return func(opts *BackupOptions) { + opts.Expand = true + } +} + +func WithoutExpand() BackupOption { + return func(opts *BackupOptions) { + opts.Expand = false + } +} + +func NewBackupOptions(opts ...BackupOption) BackupOptions { + res := GetDefaultBackupOptions() + for _, opt := range opts { + opt(&res) + } + return res +} + +func GetDefaultRestoreOptions() RestoreOptions { + return RestoreOptions{ + Replace: false, + DeactivateIndexes: false, + RestoreShadows: true, + EnforceConstraints: true, + CommitAfterEachTable: false, + UseAllPageSpace: false, + PageSize: 0, + CacheBuffers: 0, + } +} + +func WithReplace() RestoreOption { + return func(opts *RestoreOptions) { + opts.Replace = true + } +} + +func WithoutReplace() RestoreOption { + return func(opts *RestoreOptions) { + opts.Replace = false + } +} + +func WithDeactivateIndexes() RestoreOption { + return func(opts *RestoreOptions) { + opts.DeactivateIndexes = true + } +} + +func WithRestoreShadows() RestoreOption { + return func(opts *RestoreOptions) { + opts.RestoreShadows = true + } +} + +func WithoutRestoreShadows() RestoreOption { + return func(opts *RestoreOptions) { + opts.RestoreShadows = false + } +} + +func WithEnforceConstraints() RestoreOption { + return func(opts *RestoreOptions) { + opts.EnforceConstraints = true + } +} + +func WithoutEnforceConstraints() RestoreOption { + return func(opts *RestoreOptions) { + opts.EnforceConstraints = false + } +} + +func WithCommitAfterEachTable() RestoreOption { + return func(opts *RestoreOptions) { + opts.CommitAfterEachTable = true + } +} + +func WithoutCommitAfterEachTable() RestoreOption { + return func(opts *RestoreOptions) { + opts.CommitAfterEachTable = false + } +} + +func WithUseAllPageSpace() RestoreOption { + return func(opts *RestoreOptions) { + opts.UseAllPageSpace = true + } +} + +func WithoutUseAllPageSpace() RestoreOption { + return func(opts *RestoreOptions) { + opts.UseAllPageSpace = false + } +} + +func WithPageSize(pageSize int32) RestoreOption { + return func(opts *RestoreOptions) { + opts.PageSize = pageSize + } +} + +func WithCacheBuffers(cacheBuffers int32) RestoreOption { + return func(opts *RestoreOptions) { + opts.CacheBuffers = cacheBuffers + } +} + +func NewRestoreOptions(opts ...RestoreOption) RestoreOptions { + res := GetDefaultRestoreOptions() + for _, opt := range opts { + opt(&res) + } + return res +} + +func NewBackupManager(addr string, user string, password string, options ServiceManagerOptions) (*BackupManager, error) { + connBuilder := func() (*ServiceManager, error) { + return NewServiceManager(addr, user, password, options) + } + return &BackupManager{ + connBuilder: connBuilder, + }, nil +} + +func (bm *BackupManager) Backup(database string, backup string, options BackupOptions, verbose chan string) error { + var optionsMask int32 + var err error + var conn *ServiceManager + + if options.IgnoreChecksums { + optionsMask |= isc_spb_bkp_ignore_checksums + } + + if options.IgnoreLimboTransactions { + optionsMask |= isc_spb_bkp_ignore_limbo + } + + if options.MetadataOnly { + optionsMask |= isc_spb_bkp_metadata_only + } + + if !options.GarbageCollect { + optionsMask |= isc_spb_bkp_no_garbage_collect + } + + if !options.Transportable { + optionsMask |= isc_spb_bkp_non_transportable + } + + if options.ConvertExternalTablesToInternalTables { + optionsMask |= isc_spb_bkp_convert + } + + if options.Expand { + optionsMask |= isc_spb_bkp_expand + } + + spb := NewXPBWriterFromTag(isc_action_svc_backup) + spb.PutString(isc_spb_dbname, database) + spb.PutString(isc_spb_bkp_file, backup) + spb.PutInt32(isc_spb_options, optionsMask) + + if verbose != nil { + spb.PutTag(isc_spb_verbose) + } + + if conn, err = bm.connBuilder(); err != nil { + return err + } + defer func(conn *ServiceManager) { + _ = conn.Close() + }(conn) + + return bm.attach(spb.Bytes(), verbose) +} + +func (bm *BackupManager) Restore(backup string, database string, options RestoreOptions, verbose chan string) error { + var optionsMask int32 = 0 + var err error + var conn *ServiceManager + + if options.Replace { + optionsMask |= isc_spb_res_replace + } else { + optionsMask |= isc_spb_res_create + } + + if options.DeactivateIndexes { + optionsMask |= isc_spb_res_deactivate_idx + } + + if !options.RestoreShadows { + optionsMask |= isc_spb_res_no_shadow + } + + if !options.EnforceConstraints { + optionsMask |= isc_spb_res_no_validity + } + + if options.CommitAfterEachTable { + optionsMask |= isc_spb_res_one_at_a_time + } + + if options.UseAllPageSpace { + optionsMask |= isc_spb_res_use_all_space + } + + spb := NewXPBWriterFromTag(isc_action_svc_restore) + spb.PutString(isc_spb_dbname, database) + spb.PutString(isc_spb_bkp_file, backup) + spb.PutInt32(isc_spb_options, optionsMask) + + if verbose != nil { + spb.PutTag(isc_spb_verbose) + } + + if options.PageSize > 0 { + spb.PutInt32(isc_spb_res_page_size, options.PageSize) + } + + if options.CacheBuffers > 0 { + spb.PutInt32(isc_spb_res_buffers, options.PageSize) + } + + if conn, err = bm.connBuilder(); err != nil { + return err + } + defer func(conn *ServiceManager) { + _ = conn.Close() + }(conn) + + return bm.attach(spb.Bytes(), verbose) +} + +func (bm *BackupManager) attach(spb []byte, verbose chan string) error { + var err error + var conn *ServiceManager + if conn, err = bm.connBuilder(); err != nil { + return err + } + defer func(conn *ServiceManager) { + _ = conn.Close() + }(conn) + + return conn.ServiceAttach(spb, verbose) +} diff --git a/backup_manager_test.go b/backup_manager_test.go new file mode 100644 index 0000000..e492d1b --- /dev/null +++ b/backup_manager_test.go @@ -0,0 +1,61 @@ +package firebirdsql + +import ( + "database/sql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestBackupManager(t *testing.T) { + dbPathOrig := GetTestDatabase("test_backup_manager_orig_") + dbBackup := GetTestBackup("test_backup_manager_") + dbPathRest := GetTestDatabase("test_backup_manager_rest_") + conn, err := sql.Open("firebirdsql_createdb", GetTestDSNFromDatabase(dbPathOrig)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + defer conn.Close() + _, err = conn.Exec("create table test(a int)") + require.NoError(t, err, "Exec") + _, err = conn.Exec("insert into test values(123)") + require.NoError(t, err, "Exec") + + bm, err := NewBackupManager("localhost", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err, "NewBackupManager") + require.NotNil(t, bm, "NewBackupManager") + + err = bm.Backup(dbPathOrig, dbBackup, GetDefaultBackupOptions(), nil) + require.NoError(t, err, "Backup") + + err = bm.Restore(dbBackup, dbPathRest, GetDefaultRestoreOptions(), nil) + require.NoError(t, err, "Restore") + + conn, err = sql.Open("firebirdsql", GetTestDSNFromDatabase(dbPathRest)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + + rows, err := conn.Query("select * from test") + require.NoError(t, err, "Query") + require.NotNil(t, rows, "Query") + require.True(t, rows.Next(), "Next") + var res int + require.NoError(t, rows.Scan(&res), "Scan") + assert.Equal(t, 123, res, "result in restored database should be same as in original") + rows.Close() + conn.Close() + + err = bm.Restore(dbBackup, dbPathRest, GetDefaultRestoreOptions(), nil) + assert.ErrorContains(t, err, "already exists. To replace it, use the -REP switch") + + opt := GetDefaultRestoreOptions() + opt.Replace = true + err = bm.Restore(dbBackup, dbPathRest, opt, nil) + require.NoError(t, err, "Restore") +} + +func TestBackupOptions(t *testing.T) { + opts := NewBackupOptions() + assert.Equal(t, BackupOptions{IgnoreChecksums: false, IgnoreLimboTransactions: false, MetadataOnly: false, GarbageCollect: true, Transportable: true, ConvertExternalTablesToInternalTables: true, Expand: false}, opts) + opts = NewBackupOptions(WithIgnoreChecksums(), WithIgnoreLimboTransactions(), WithMetadataOnly(), WithoutGarbageCollect(), WithoutTransportable(), WithoutConvertExternalTablesToInternalTables(), WithExpand()) + assert.Equal(t, BackupOptions{IgnoreChecksums: true, IgnoreLimboTransactions: true, MetadataOnly: true, GarbageCollect: false, Transportable: false, ConvertExternalTablesToInternalTables: false, Expand: true}, opts) +} diff --git a/consts.go b/consts.go index 4b54284..36ea7b3 100644 --- a/consts.go +++ b/consts.go @@ -175,27 +175,50 @@ const ( isc_tpb_lock_timeout = 21 // Service Parameter Block parameter - isc_spb_version1 = 1 - isc_spb_current_version = 2 - isc_spb_version = isc_spb_current_version - isc_spb_user_name = 28 // isc_dpb_user_name - isc_spb_sys_user_name = 19 // isc_dpb_sys_user_name - isc_spb_sys_user_name_enc = 31 // isc_dpb_sys_user_name_enc - isc_spb_password = 29 // isc_dpb_password - isc_spb_password_enc = 30 // isc_dpb_password_enc - isc_spb_command_line = 105 - isc_spb_dbname = 106 - isc_spb_verbose = 107 - isc_spb_options = 108 - isc_spb_address_path = 109 - isc_spb_process_id = 110 - isc_spb_trusted_auth = 111 - isc_spb_process_name = 112 - isc_spb_trusted_role = 113 + isc_spb_version1 = 1 + isc_spb_current_version = 2 + isc_spb_version = isc_spb_current_version + isc_spb_user_name = 28 // isc_dpb_user_name + isc_spb_sys_user_name = 19 // isc_dpb_sys_user_name + isc_spb_sys_user_name_enc = 31 // isc_dpb_sys_user_name_enc + isc_spb_password = 29 // isc_dpb_password + isc_spb_password_enc = 30 // isc_dpb_password_enc + isc_spb_command_line = 105 + isc_spb_dbname = 106 + isc_spb_verbose = 107 + isc_spb_options = 108 + isc_spb_address_path = 109 + isc_spb_process_id = 110 + isc_spb_trusted_auth = 111 + isc_spb_process_name = 112 + isc_spb_trusted_role = 113 + isc_spb_verbint = 114 + isc_spb_auth_block = 115 + isc_spb_auth_plugin_name = 116 + isc_spb_auth_plugin_list = 117 + isc_spb_utf8_filename = 118 + isc_spb_client_version = 119 + isc_spb_remote_protocol = 120 + isc_spb_host_name = 121 + isc_spb_os_user = 122 + isc_spb_config = 123 + isc_spb_expected_db = 124 + isc_spb_connect_timeout = 57 // isc_dpb_connect_timeout isc_spb_dummy_packet_interval = 58 // isc_dpb_dummy_packet_interval isc_spb_sql_role_name = 60 // isc_dpb_sql_role_name + // Parameters for isc_action_{add|del|mod|disp)_user + isc_spb_sec_userid = 5 + isc_spb_sec_groupid = 6 + isc_spb_sec_username = 7 + isc_spb_sec_password = 8 + isc_spb_sec_groupname = 9 + isc_spb_sec_firstname = 10 + isc_spb_sec_middlename = 11 + isc_spb_sec_lastname = 12 + isc_spb_sec_admin = 13 + //Database Parameter Block Types isc_dpb_version1 = 1 isc_dpb_page_size = 4 @@ -252,6 +275,83 @@ const ( isc_spb_res_create = 0x2000 isc_spb_res_use_all_space = 0x4000 + // Parameters for isc_action_svc_nbak + isc_spb_nbk_level = 5 + isc_spb_nbk_file = 6 + isc_spb_nbk_direct = 7 + isc_spb_nbk_guid = 8 + isc_spb_nbk_clean_history = 9 + isc_spb_nbk_keep_days = 10 + isc_spb_nbk_keep_rows = 11 + isc_spb_nbk_no_triggers = 0x01 + isc_spb_nbk_inplace = 0x02 + isc_spb_nbk_sequence = 0x04 + + // Parameters for isc_action_svc_properties + isc_spb_prp_page_buffers = 5 + isc_spb_prp_sweep_interval = 6 + isc_spb_prp_shutdown_db = 7 + isc_spb_prp_deny_new_attachments = 9 + isc_spb_prp_deny_new_transactions = 10 + isc_spb_prp_reserve_space = 11 + isc_spb_prp_write_mode = 12 + isc_spb_prp_access_mode = 13 + isc_spb_prp_set_sql_dialect = 14 + isc_spb_prp_activate = 0x0100 + isc_spb_prp_db_online = 0x0200 + isc_spb_prp_nolinger = 0x0400 + isc_spb_prp_force_shutdown = 41 + isc_spb_prp_attachments_shutdown = 42 + isc_spb_prp_transactions_shutdown = 43 + isc_spb_prp_shutdown_mode = 44 + isc_spb_prp_online_mode = 45 + isc_spb_prp_replica_mode = 46 + + // Parameters for isc_spb_prp_shutdown_mode and isc_spb_prp_online_mode + isc_spb_prp_sm_normal = 0 + isc_spb_prp_sm_multi = 1 + isc_spb_prp_sm_single = 2 + isc_spb_prp_sm_full = 3 + + // Parameters for isc_action_svc_repair + isc_spb_rpr_commit_trans = 15 + isc_spb_rpr_rollback_trans = 34 + isc_spb_rpr_recover_two_phase = 17 + isc_spb_tra_id = 18 + isc_spb_single_tra_id = 19 + isc_spb_multi_tra_id = 20 + isc_spb_tra_state = 21 + isc_spb_tra_state_limbo = 22 + isc_spb_tra_state_commit = 23 + isc_spb_tra_state_rollback = 24 + isc_spb_tra_state_unknown = 25 + isc_spb_tra_host_site = 26 + isc_spb_tra_remote_site = 27 + isc_spb_tra_db_path = 28 + isc_spb_tra_advise = 29 + isc_spb_tra_advise_commit = 30 + isc_spb_tra_advise_rollback = 31 + isc_spb_tra_advise_unknown = 33 + isc_spb_tra_id_64 = 46 + isc_spb_single_tra_id_64 = 47 + isc_spb_multi_tra_id_64 = 48 + isc_spb_rpr_commit_trans_64 = 49 + isc_spb_rpr_rollback_trans_64 = 50 + isc_spb_rpr_recover_two_phase_64 = 51 + isc_spb_rpr_par_workers = 52 + + // Parameters for isc_spb_prp_reserve_space * + isc_spb_prp_res_use_full = 35 + isc_spb_prp_res = 36 + + // Parameters for isc_spb_prp_write_mode + isc_spb_prp_wm_async = 37 + isc_spb_prp_wm_sync = 38 + + // Parameters for isc_spb_prp_access_mode + isc_spb_prp_am_readonly = 39 + isc_spb_prp_am_readwrite = 40 + // trace isc_spb_trc_id = 1 isc_spb_trc_name = 2 @@ -305,7 +405,9 @@ const ( isc_action_svc_set_mapping = 27 isc_action_svc_drop_mapping = 28 isc_action_svc_display_user_adm = 29 - isc_action_svc_last = 30 + isc_action_svc_validate = 30 + isc_action_svc_nfix = 31 + isc_action_svc_last = 32 // Transaction informatino items isc_info_tra_id = 4 @@ -455,3 +557,34 @@ const ( fb_cancel_raise = 3 fb_cancel_abort = 4 ) + +type ShutdownMode byte + +const ( + //ShutdownModeForce Wait for N seconds then shutdown + ShutdownModeForce ShutdownMode = isc_spb_prp_shutdown_db + //ShutdownModeDenyNewAttachments Disable new attachments for N seconds then shutdown + ShutdownModeDenyNewAttachments ShutdownMode = isc_spb_prp_deny_new_attachments + //ShutdownModeDenyNewTransactions Disable new transactions for N seconds then shutdown + ShutdownModeDenyNewTransactions ShutdownMode = isc_spb_prp_deny_new_transactions +) + +type OperationMode byte + +const ( + OperationModeNormal OperationMode = isc_spb_prp_sm_normal + OperationModeMulti OperationMode = isc_spb_prp_sm_multi + OperationModeSingle OperationMode = isc_spb_prp_sm_single + OperationModeFull OperationMode = isc_spb_prp_sm_full +) + +type ShutdownModeEx byte + +const ( + //ShutdownModeExForce Wait for N seconds then shutdown + ShutdownModeExForce ShutdownModeEx = isc_spb_prp_force_shutdown + //ShutdownModeExDenyNewAttachments Disable new attachments for N seconds then shutdown + ShutdownModeExDenyNewAttachments ShutdownModeEx = isc_spb_prp_attachments_shutdown + //ShutdownModeExDenyNewTransactions Disable new transactions for N seconds then shutdown + ShutdownModeExDenyNewTransactions ShutdownModeEx = isc_spb_prp_transactions_shutdown +) diff --git a/driver_test.go b/driver_test.go index 4d26296..051313a 100644 --- a/driver_test.go +++ b/driver_test.go @@ -67,35 +67,72 @@ var ( end` ) -func get_firebird_major_version(conn *sql.DB) int { - var s string - conn.QueryRow("SELECT rdb$get_context('SYSTEM', 'ENGINE_VERSION') from rdb$database").Scan(&s) - major_version, _ := strconv.Atoi(s[:strings.Index(s, ".")]) - return major_version +func get_firebird_major_version(t *testing.T) int { + sm, err := NewServiceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, sm) + defer sm.Close() + version, err := sm.GetServerVersion() + require.NoError(t, err) + return version.Major } -func GetTestDSN(prefix string) string { - var tmppath string +func GetTestDatabase(prefix string) string { + randBytes := make([]byte, 16) + rand.Read(randBytes) + return filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+".fdb") +} + +func GetTestBackup(prefix string) string { randBytes := make([]byte, 16) rand.Read(randBytes) + return filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+".fbk") +} + +func GetTestDSN(prefix string) string { + return GetTestDSNFromDatabase(GetTestDatabase(prefix)) +} - tmppath = filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+".fdb") +func GetTestDSNFromDatabase(dbPath string) string { + return GetTestDSNFromDatabaseUserPassword(dbPath, GetTestUser(), GetTestPassword()) +} + +func GetTestDSNFromDatabaseUserPassword(dbPath string, testUser string, testPassword string) string { if runtime.GOOS == "windows" { - tmppath = "/" + tmppath + dbPath = "/" + dbPath } + return testUser + ":" + testPassword + "@localhost:3050" + dbPath +} - test_user := "sysdba" - if isc_user := os.Getenv("ISC_USER"); isc_user != "" { - test_user = isc_user +func GetTestUser() string { + testUser := "sysdba" + if iscUser := os.Getenv("ISC_USER"); iscUser != "" { + testUser = iscUser } + return testUser +} - test_password := "masterkey" - if isc_password := os.Getenv("ISC_PASSWORD"); isc_password != "" { - test_password = isc_password +func GetTestPassword() string { + testPassword := "masterkey" + if iscPassword := os.Getenv("ISC_PASSWORD"); iscPassword != "" { + testPassword = iscPassword } + return testPassword +} - retorno := test_user + ":" + test_password + "@localhost:3050" - return retorno + tmppath +func CreateTestDatabase(prefix string) (file string, dsn string, err error) { + file = GetTestDatabase(prefix) + dsn = GetTestDSNFromDatabase(file) + conn, err := sql.Open("firebirdsql_createdb", dsn) + if err != nil { + return + } + defer conn.Close() + _, err = conn.Exec("select * from rdb$database") + if err != nil { + return + } + return } func TestBasic(t *testing.T) { @@ -454,7 +491,7 @@ func TestBoolean(t *testing.T) { t.Fatalf("Error connecting: %v", err) } - firebird_major_version := get_firebird_major_version(conn) + firebird_major_version := get_firebird_major_version(t) if firebird_major_version < 3 { return } @@ -513,7 +550,7 @@ func TestDecFloat(t *testing.T) { t.Fatalf("Error connecting: %v", err) } - firebird_major_version := get_firebird_major_version(conn) + firebird_major_version := get_firebird_major_version(t) if firebird_major_version < 4 { return } @@ -567,7 +604,7 @@ func TestTimeZone(t *testing.T) { t.Fatalf("Error connecting: %v", err) } - firebird_major_version := get_firebird_major_version(conn) + firebird_major_version := get_firebird_major_version(t) if firebird_major_version < 4 { return } @@ -612,7 +649,7 @@ func TestInt128(t *testing.T) { t.Fatalf("Error connecting: %v", err) } - firebird_major_version := get_firebird_major_version(conn) + firebird_major_version := get_firebird_major_version(t) if firebird_major_version < 4 { return } @@ -647,7 +684,7 @@ func TestNegativeInt128(t *testing.T) { t.Fatalf("Error connecting: %v", err) } - firebird_major_version := get_firebird_major_version(conn) + firebird_major_version := get_firebird_major_version(t) if firebird_major_version < 4 { return } @@ -1286,7 +1323,7 @@ func TestGoIssue172(t *testing.T) { testDsn := GetTestDSN("test_constraint_type_") conn, err := sql.Open("firebirdsql_createdb", testDsn) require.NoError(t, err) - firebird_major_version := get_firebird_major_version(conn) + firebird_major_version := get_firebird_major_version(t) if firebird_major_version < 3 { return } diff --git a/firebird_version.go b/firebird_version.go new file mode 100644 index 0000000..a053949 --- /dev/null +++ b/firebird_version.go @@ -0,0 +1,67 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +import ( + "regexp" + "strconv" +) + +var FirebirdVersionPattern = regexp.MustCompile(`((\w{2})-(\w)(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-\S+)?) (.+)`) + +type FirebirdVersion struct { + Platform string + Type string + Full string + Major int + Minor int + Patch int + BuildNumber int + Raw string +} + +func ParseFirebirdVersion(rawVersionString string) FirebirdVersion { + res := FirebirdVersionPattern.FindStringSubmatch(rawVersionString) + major, _ := strconv.Atoi(res[4]) + minor, _ := strconv.Atoi(res[5]) + patch, _ := strconv.Atoi(res[6]) + build, _ := strconv.Atoi(res[7]) + return FirebirdVersion{Platform: res[2], + Type: res[3], + Full: res[1], + Major: major, + Minor: minor, + Patch: patch, + BuildNumber: build, + Raw: rawVersionString, + } +} + +func (v FirebirdVersion) EqualOrGreater(major int, minor int) bool { + return v.Major > major || (v.Major == major && v.Minor >= minor) +} + +func (v FirebirdVersion) EqualOrGreaterPatch(major int, minor int, patch int) bool { + return v.Major > major || (v.Major == major && (v.Minor == minor && v.Patch >= patch || v.Minor > minor)) +} diff --git a/maintenance_manager.go b/maintenance_manager.go new file mode 100644 index 0000000..d163e28 --- /dev/null +++ b/maintenance_manager.go @@ -0,0 +1,289 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +type MaintenanceManager struct { + connBuilder func() (*ServiceManager, error) +} + +func NewMaintenanceManager(addr string, user string, password string, options ServiceManagerOptions) (*MaintenanceManager, error) { + connBuilder := func() (*ServiceManager, error) { + return NewServiceManager(addr, user, password, options) + } + return &MaintenanceManager{ + connBuilder: connBuilder, + }, nil +} + +func (mm *MaintenanceManager) setAccessMode(database string, mode byte) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutByte(isc_spb_prp_access_mode, mode) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) SetAccessModeReadWrite(database string) error { + return mm.setAccessMode(database, isc_spb_prp_am_readwrite) +} + +func (mm *MaintenanceManager) SetAccessModeReadOnly(database string) error { + return mm.setAccessMode(database, isc_spb_prp_am_readonly) +} + +func (mm *MaintenanceManager) SetDialect(database string, dialect int) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_prp_set_sql_dialect, int32(dialect)) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) SetPageBuffers(database string, pageCount int) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_prp_page_buffers, int32(pageCount)) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) setWriteMode(database string, mode byte) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutByte(isc_spb_prp_write_mode, mode) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) SetWriteModeAsync(database string) error { + return mm.setWriteMode(database, isc_spb_prp_wm_async) +} + +func (mm *MaintenanceManager) SetWriteModeSync(database string) error { + return mm.setWriteMode(database, isc_spb_prp_wm_sync) +} + +func (mm *MaintenanceManager) setPageFill(database string, mode byte) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutByte(isc_spb_prp_reserve_space, mode) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) SetPageFillNoReserve(database string) error { + return mm.setPageFill(database, isc_spb_prp_res_use_full) +} + +func (mm *MaintenanceManager) SetPageFillReserve(database string) error { + return mm.setPageFill(database, isc_spb_prp_res) +} + +func (mm *MaintenanceManager) Shutdown(database string, shutdownMode ShutdownMode, timeout uint) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(byte(shutdownMode), int32(timeout)) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) Online(database string) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, isc_spb_prp_db_online) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) NoLinger(database string) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, isc_spb_prp_nolinger) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) ShutdownEx(database string, operationMode OperationMode, shutdownModeEx ShutdownModeEx, timeout uint) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutByte(isc_spb_prp_shutdown_mode, byte(operationMode)) + spb.PutInt32(byte(shutdownModeEx), int32(timeout)) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) OnlineEx(database string, operationMode OperationMode) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutByte(isc_spb_prp_online_mode, byte(operationMode)) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) SetSweepInterval(database string, transactions uint) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_prp_sweep_interval, int32(transactions)) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) Sweep(database string) error { + spb := NewXPBWriterFromTag(isc_action_svc_repair) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, isc_spb_rpr_sweep_db) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) ActivateShadow(shadow string) error { + spb := NewXPBWriterFromTag(isc_action_svc_properties) + spb.PutString(isc_spb_dbname, shadow) + spb.PutInt32(isc_spb_options, isc_spb_prp_activate) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) KillShadow(database string) error { + spb := NewXPBWriterFromTag(isc_action_svc_repair) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, isc_spb_rpr_kill_shadows) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) Mend(database string) error { + spb := NewXPBWriterFromTag(isc_action_svc_repair) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, isc_spb_rpr_mend_db) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) Validate(database string, options int) error { + spb := NewXPBWriterFromTag(isc_action_svc_repair) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, isc_spb_rpr_validate_db|int32(options)) + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) GetLimboTransactions(database string) ([]int64, error) { + var ( + err error + resChan = make(chan []byte) + done = make(chan bool) + cont = true + buf []byte + tids []int64 + ) + + spb := NewXPBWriterFromTag(isc_action_svc_repair) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, isc_spb_rpr_list_limbo_trans) + + go func() { + err = mm.attachBuffer(spb.Bytes(), resChan) + done <- true + }() + + for cont { + select { + case buf = <-resChan: + srb := NewXPBReader(buf) + var ( + have bool + val byte + ) + for { + if have, val = srb.Next(); !have { + break + } + switch val { + case isc_spb_single_tra_id: + fallthrough + case isc_spb_multi_tra_id: + tids = append(tids, int64(srb.GetInt32())) + case isc_spb_single_tra_id_64: + fallthrough + case isc_spb_multi_tra_id_64: + tids = append(tids, srb.GetInt64()) + case isc_spb_tra_id: + srb.Skip(4) + case isc_spb_tra_id_64: + srb.Skip(8) + case isc_spb_tra_state: + fallthrough + case isc_spb_tra_advise: + srb.Skip(1) + case isc_spb_tra_host_site: + fallthrough + case isc_spb_tra_remote_site: + fallthrough + case isc_spb_tra_db_path: + srb.GetString() + } + } + case <-done: + cont = false + break + } + } + + return tids, err +} + +func (mm *MaintenanceManager) CommitTransaction(database string, transaction int64) error { + spb := NewXPBWriterFromTag(isc_action_svc_repair) + spb.PutString(isc_spb_dbname, database) + if fitsUint32(transaction) { + spb.PutInt32(isc_spb_rpr_commit_trans, int32(transaction)) + } else { + spb.PutInt64(isc_spb_rpr_commit_trans_64, transaction) + } + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) RollbackTransaction(database string, transaction int64) error { + spb := NewXPBWriterFromTag(isc_action_svc_repair) + spb.PutString(isc_spb_dbname, database) + if fitsUint32(transaction) { + spb.PutInt32(isc_spb_rpr_rollback_trans, int32(transaction)) + } else { + spb.PutInt64(isc_spb_rpr_rollback_trans_64, transaction) + } + return mm.attach(spb.Bytes(), nil) +} + +func (mm *MaintenanceManager) attach(spb []byte, verbose chan string) error { + var ( + err error + conn *ServiceManager + ) + if conn, err = mm.connBuilder(); err != nil { + return err + } + defer func(conn *ServiceManager) { + _ = conn.Close() + }(conn) + return conn.ServiceAttach(spb, verbose) +} + +func (mm *MaintenanceManager) attachBuffer(spb []byte, verbose chan []byte) error { + var ( + err error + conn *ServiceManager + ) + if conn, err = mm.connBuilder(); err != nil { + return err + } + defer func(conn *ServiceManager) { + _ = conn.Close() + }(conn) + return conn.ServiceAttachBuffer(spb, verbose) +} diff --git a/maintenance_manager_test.go b/maintenance_manager_test.go new file mode 100644 index 0000000..c8a3403 --- /dev/null +++ b/maintenance_manager_test.go @@ -0,0 +1,344 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path" + "regexp" + "testing" +) + +func cleanFirebirdLog(t *testing.T) { + m, err := NewServiceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + defer m.Close() + + var logFile string + if logFile = os.Getenv("FIREBIRD_LOG"); logFile == "" { + logFile, err = m.GetHomeDir() + require.NoError(t, err) + logFile = path.Join(logFile, "firebird.log") + } + + _, err = os.Stat(logFile) + if os.IsNotExist(err) { + return + } + require.NoError(t, os.Truncate(logFile, 0)) +} + +func getFirebirdLog(t *testing.T) string { + m, err := NewServiceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + defer m.Close() + log, err := m.GetFbLogString() + require.NoError(t, err) + log = regexp.MustCompile(`(Database).*`).ReplaceAllString(log, "$1 xxxxx") + log = regexp.MustCompile(`\w+\s+\w+\s+\w+\s+\d+\s+\d+:\d+:\d+\s+\d+`).ReplaceAllString(log, "") + log = regexp.MustCompile(`(?m)^\s+`).ReplaceAllString(log, "") + log = regexp.MustCompile(`(?m)\s+$`).ReplaceAllString(log, "") + log = regexp.MustCompile(`(OIT|OAT|OST|Next) \d+`).ReplaceAllString(log, "$1 xxx") + log = regexp.MustCompile(`\d+ (workers|errors|warnings|fixed)`).ReplaceAllString(log, "x $1") + log = regexp.MustCompile(`(time) \d+\.\d+`).ReplaceAllString(log, "$1 x.xxx") + return log +} + +func grabStringOutput(run func() error, resChan chan string) (string, error) { + done := make(chan bool) + var result string + var err error + + go func() { + err = run() + done <- true + }() + + for loop, s := true, ""; loop; { + select { + case <-done: + loop = false + break + case s = <-resChan: + result += s + "\n" + } + } + return result, err +} + +func TestServiceManager_Sweep(t *testing.T) { + if get_firebird_major_version(t) < 3 { + t.Skip("skip for 2.5, because it running in container") + } + + db, _, err := CreateTestDatabase("test_sweep_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + cleanFirebirdLog(t) + err = m.Sweep(db) + assert.NoError(t, err) + log := getFirebirdLog(t) + fmt.Println(log) + assert.Contains(t, log, `Sweep is started by SYSDBA +Database xxxxx +OIT xxx, OAT xxx, OST xxx, Next xxx`) + assert.Contains(t, log, `Sweep is finished +Database xxxxx +OIT xxx, OAT xxx, OST xxx, Next xxx`) +} + +func TestServiceManager_Validate(t *testing.T) { + if get_firebird_major_version(t) < 3 { + t.Skip("skip for 2.5, because it running in container") + } + + db, _, err := CreateTestDatabase("test_validate_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + + cleanFirebirdLog(t) + err = m.Validate(db, isc_spb_rpr_check_db) + assert.NoError(t, err) + log := getFirebirdLog(t) + assert.Contains(t, log, `Database xxxxx +Validation started`) + assert.Contains(t, log, `Database xxxxx +Validation finished: x errors, x warnings, x fixed`) + + cleanFirebirdLog(t) + err = m.Validate(db, isc_spb_rpr_full) + assert.NoError(t, err) + log = getFirebirdLog(t) + assert.Contains(t, log, `Database xxxxx +Validation started`) + assert.Contains(t, log, `Database xxxxx +Validation finished: x errors, x warnings, x fixed`) +} + +func TestServiceManager_Mend(t *testing.T) { + if get_firebird_major_version(t) < 3 { + t.Skip("skip for 2.5, because it running in container") + } + + db, _, err := CreateTestDatabase("test_mend_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + + cleanFirebirdLog(t) + err = m.Mend(db) + assert.NoError(t, err) + log := getFirebirdLog(t) + assert.Contains(t, log, `Database xxxxx +Validation started`) + assert.Contains(t, log, `Database xxxxx +Validation finished: x errors, x warnings, x fixed`) +} + +func TestServiceManager_ListLimboTransactions(t *testing.T) { + db, _, err := CreateTestDatabase("test_list_limbo_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + _, err = m.GetLimboTransactions(db) + assert.NoError(t, err) +} + +func TestServiceManager_CommitTransaction(t *testing.T) { + db, _, err := CreateTestDatabase("test_commit_transaction_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + err = m.CommitTransaction(db, 1) + if err == nil && get_firebird_major_version(t) < 3 { + //FIXME: Not sure if bug + t.Log("This tests should fail, but on 2.5 it passing. Ignoring this error as 2.5 is obsolete") + return + } + assert.EqualError(t, err, fmt.Sprintf(`failed to reconnect to a transaction in database %s +transaction is not in limbo +transaction 1 is committed +`, db)) +} + +func TestServiceManager_RollbackTransaction(t *testing.T) { + db, _, err := CreateTestDatabase("test_rollback_transaction_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + err = m.RollbackTransaction(db, 1) + if err == nil && get_firebird_major_version(t) < 3 { + //FIXME: Not sure if bug + t.Log("This tests should fail, but on 2.5 it passing. Ignoring this error as 2.5 is obsolete") + return + } + assert.EqualError(t, err, fmt.Sprintf(`failed to reconnect to a transaction in database %s +transaction is not in limbo +transaction 1 is committed +`, db)) +} + +func TestServiceManager_SetDatabaseMode(t *testing.T) { + db, _, err := CreateTestDatabase("test_set_mode_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + err = m.SetAccessModeReadOnly(db) + assert.NoError(t, err) + err = m.SetAccessModeReadWrite(db) + assert.NoError(t, err) +} + +func TestServiceManager_SetDatabaseDialect(t *testing.T) { + db, _, err := CreateTestDatabase("test_set_dialect_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + err = m.SetDialect(db, 1) + assert.NoError(t, err) + err = m.SetDialect(db, 3) + assert.NoError(t, err) + err = m.SetDialect(db, 10) + assert.Error(t, err) +} + +func TestServiceManager_SetPageBuffers(t *testing.T) { + db, _, err := CreateTestDatabase("test_set_buffers_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + err = m.SetPageBuffers(db, 0) + assert.NoError(t, err) + err = m.SetPageBuffers(db, 30) + assert.Error(t, err) + err = m.SetPageBuffers(db, 100) + assert.NoError(t, err) +} + +func TestServiceManager_SetWriteMode(t *testing.T) { + db, _, err := CreateTestDatabase("test_set_write_mode_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + err = m.SetWriteModeAsync(db) + assert.NoError(t, err) + err = m.SetWriteModeSync(db) + assert.NoError(t, err) +} + +func TestServiceManager_SetPageFill(t *testing.T) { + db, _, err := CreateTestDatabase("test_set_page_fill_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + err = m.SetPageFillNoReserve(db) + assert.NoError(t, err) + err = m.SetPageFillReserve(db) + assert.NoError(t, err) +} + +func TestServiceManager_DatabaseShutdown(t *testing.T) { + db, _, err := CreateTestDatabase("test_shutdown_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + + for _, mode := range []ShutdownMode{ShutdownModeDenyNewAttachments, ShutdownModeDenyNewTransactions, ShutdownModeForce} { + err = m.Shutdown(db, mode, 0) + assert.NoError(t, err) + err = m.Online(db) + assert.NoError(t, err) + } +} + +func TestServiceManager_DatabaseShutdownEx(t *testing.T) { + db, _, err := CreateTestDatabase("test_shutdown_ex_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + + err = m.ShutdownEx(db, OperationModeFull, ShutdownModeExForce, 0) + assert.NoError(t, err) + err = m.OnlineEx(db, OperationModeNormal) + assert.NoError(t, err) +} + +func TestServiceManager_SetSweepInterval(t *testing.T) { + db, _, err := CreateTestDatabase("test_set_sweep_interval_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + + err = m.SetSweepInterval(db, 20000) + assert.NoError(t, err) +} + +func TestServiceManager_NoLinger(t *testing.T) { + if get_firebird_major_version(t) < 3 { + t.Skip("firebird 2.5 do not support isc_spb_prp_nolinger") + } + + db, _, err := CreateTestDatabase("test_nolinger_") + require.NoError(t, err) + + m, err := NewMaintenanceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err) + require.NotNil(t, m) + + err = m.NoLinger(db) + assert.NoError(t, err) +} diff --git a/nbackup_manager.go b/nbackup_manager.go new file mode 100644 index 0000000..41a4ff3 --- /dev/null +++ b/nbackup_manager.go @@ -0,0 +1,191 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +type NBackupManager struct { + connBuilder func() (*ServiceManager, error) +} + +type NBackupOptions struct { + Level int32 + Guid string + NoDBTriggers bool + InPlaceRestore bool + PreserveSequence bool +} + +type NBackupOption func(*NBackupOptions) + +func GetDefaultNBackupOptions() NBackupOptions { + return NBackupOptions{ + Level: -1, + Guid: "", + NoDBTriggers: false, + InPlaceRestore: false, + PreserveSequence: false, + } +} + +func (o NBackupOptions) GetOptionsMask() int32 { + var optionsMask int32 + + if o.NoDBTriggers { + optionsMask |= isc_spb_nbk_no_triggers + } + if o.InPlaceRestore { + optionsMask |= isc_spb_nbk_inplace + } + if o.PreserveSequence { + optionsMask |= isc_spb_nbk_sequence + } + + return optionsMask +} + +func WithLevel(level int) NBackupOption { + return func(opts *NBackupOptions) { + opts.Level = int32(level) + } +} + +func WithGuid(guid string) NBackupOption { + return func(opts *NBackupOptions) { + opts.Guid = guid + } +} + +func WithoutDBTriggers() NBackupOption { + return func(opts *NBackupOptions) { + opts.NoDBTriggers = true + } +} + +func WithDBTriggers() NBackupOption { + return func(opts *NBackupOptions) { + opts.NoDBTriggers = false + } +} + +func WithInPlaceRestore() NBackupOption { + return func(opts *NBackupOptions) { + opts.InPlaceRestore = false + } +} + +func WithPlaceRestore() NBackupOption { + return func(opts *NBackupOptions) { + opts.InPlaceRestore = true + } +} + +func WithPreserveSequence() NBackupOption { + return func(opts *NBackupOptions) { + opts.PreserveSequence = true + } +} + +func WithoutPreserveSequence() NBackupOption { + return func(opts *NBackupOptions) { + opts.PreserveSequence = false + } +} + +func NewNBackupOptions(opts ...NBackupOption) NBackupOptions { + res := GetDefaultNBackupOptions() + for _, opt := range opts { + opt(&res) + } + return res +} + +func NewNBackupManager(addr string, user string, password string, options ServiceManagerOptions) (*NBackupManager, error) { + connBuilder := func() (*ServiceManager, error) { + return NewServiceManager(addr, user, password, options) + } + return &NBackupManager{ + connBuilder: connBuilder, + }, nil +} + +func (bm *NBackupManager) Backup(database string, backup string, options NBackupOptions, verbose chan string) error { + spb := NewXPBWriterFromTag(isc_action_svc_nbak) + spb.PutString(isc_spb_dbname, database) + spb.PutString(isc_spb_nbk_file, backup) + + level := options.Level + if options.Level < 0 && options.Guid == "" { + level = 0 + } + spb.PutInt32(isc_spb_nbk_level, level) + if options.Guid != "" { + spb.PutString(isc_spb_nbk_guid, options.Guid) + } + + optionsMask := options.GetOptionsMask() + if optionsMask != 0 { + spb.PutInt32(isc_spb_options, optionsMask) + } + + return bm.attach(spb.Bytes(), verbose) +} + +func (bm *NBackupManager) Restore(backups []string, database string, options NBackupOptions, verbose chan string) error { + spb := NewXPBWriterFromTag(isc_action_svc_nrest) + spb.PutString(isc_spb_dbname, database) + for _, file := range backups { + spb.PutString(isc_spb_nbk_file, file) + } + + optionsMask := options.GetOptionsMask() + if optionsMask != 0 { + spb.PutInt32(isc_spb_options, optionsMask) + } + + return bm.attach(spb.Bytes(), verbose) +} + +func (bm *NBackupManager) Fixup(database string, options NBackupOptions, verbose chan string) error { + spb := NewXPBWriterFromTag(isc_action_svc_nfix) + spb.PutString(isc_spb_dbname, database) + + optionsMask := options.GetOptionsMask() + if optionsMask != 0 { + spb.PutInt32(isc_spb_options, optionsMask) + } + + return bm.attach(spb.Bytes(), verbose) +} + +func (bm *NBackupManager) attach(spb []byte, verbose chan string) error { + var err error + var conn *ServiceManager + if conn, err = bm.connBuilder(); err != nil { + return err + } + defer func(conn *ServiceManager) { + _ = conn.Close() + }(conn) + + return conn.ServiceAttach(spb, verbose) +} diff --git a/nbackup_manager_test.go b/nbackup_manager_test.go new file mode 100644 index 0000000..c978524 --- /dev/null +++ b/nbackup_manager_test.go @@ -0,0 +1,174 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +import ( + "database/sql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNBackupManagerSingleLevel(t *testing.T) { + dbPathOrig := GetTestDatabase("test_nbackup_manager_orig_") + dbBackup := GetTestBackup("test_nbackup_manager_") + dbPathRest := GetTestDatabase("test_nbackup_manager_rest_") + conn, err := sql.Open("firebirdsql_createdb", GetTestDSNFromDatabase(dbPathOrig)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + defer conn.Close() + _, err = conn.Exec("create table test(a int)") + require.NoError(t, err, "Exec") + _, err = conn.Exec("insert into test values(123)") + require.NoError(t, err, "Exec") + + bm, err := NewNBackupManager("localhost", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err, "NewBackupManager") + require.NotNil(t, bm, "NewBackupManager") + + err = bm.Backup(dbPathOrig, dbBackup, GetDefaultNBackupOptions(), nil) + require.NoError(t, err, "Backup") + + err = bm.Restore([]string{dbBackup}, dbPathRest, GetDefaultNBackupOptions(), nil) + require.NoError(t, err, "Restore") + + conn, err = sql.Open("firebirdsql", GetTestDSNFromDatabase(dbPathRest)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + + rows, err := conn.Query("select * from test") + require.NoError(t, err, "Query") + require.NotNil(t, rows, "Query") + require.True(t, rows.Next(), "Next") + var res int + require.NoError(t, rows.Scan(&res), "Scan") + assert.Equal(t, 123, res, "result in restored database should be same as in original") + rows.Close() + conn.Close() +} + +func TestNBackupManagerFixup(t *testing.T) { + if get_firebird_major_version(t) < 4 { + t.Skip("fixup in Service Manager API supported since 4.0") + } + + dbPathOrig := GetTestDatabase("test_nbackup_manager_orig_") + dbBackup := GetTestBackup("test_nbackup_manager_") + conn, err := sql.Open("firebirdsql_createdb", GetTestDSNFromDatabase(dbPathOrig)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + defer conn.Close() + _, err = conn.Exec("create table test(a int)") + require.NoError(t, err, "Exec") + _, err = conn.Exec("insert into test values(123)") + require.NoError(t, err, "Exec") + + bm, err := NewNBackupManager("localhost", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err, "NewBackupManager") + require.NotNil(t, bm, "NewBackupManager") + + err = bm.Backup(dbPathOrig, dbBackup, GetDefaultNBackupOptions(), nil) + require.NoError(t, err, "Backup") + + err = bm.Fixup(dbBackup, GetDefaultNBackupOptions(), nil) + require.NoError(t, err, "Fixup") + + conn, err = sql.Open("firebirdsql", GetTestDSNFromDatabase(dbBackup)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + + rows, err := conn.Query("select * from test") + require.NoError(t, err, "Query") + require.NotNil(t, rows, "Query") + require.True(t, rows.Next(), "Next") + var res int + require.NoError(t, rows.Scan(&res), "Scan") + assert.Equal(t, 123, res, "result in restored database should be same as in original") + rows.Close() + conn.Close() +} + +func TestNBackupManagerIncremental(t *testing.T) { + dbPathOrig := GetTestDatabase("test_nbackup_manager_orig_") + dbBackup0 := GetTestBackup("test_nbackup_manager_") + dbBackup1 := GetTestBackup("test_nbackup_manager_") + dbPathRest := GetTestDatabase("test_nbackup_manager_rest_") + conn, err := sql.Open("firebirdsql_createdb", GetTestDSNFromDatabase(dbPathOrig)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + defer conn.Close() + _, err = conn.Exec("create table test(a int)") + require.NoError(t, err, "Exec") + _, err = conn.Exec("insert into test values(123)") + require.NoError(t, err, "Exec") + + bm, err := NewNBackupManager("localhost", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err, "NewBackupManager") + require.NotNil(t, bm, "NewBackupManager") + + opt := GetDefaultNBackupOptions() + opt.Level = 0 + err = bm.Backup(dbPathOrig, dbBackup0, opt, nil) + require.NoError(t, err, "Backup Level 0") + + _, err = conn.Exec("insert into test values(456)") + require.NoError(t, err, "Exec") + + opt.Level = 1 + err = bm.Backup(dbPathOrig, dbBackup1, opt, nil) + require.NoError(t, err, "Backup Level 1") + + err = bm.Restore([]string{dbBackup0, dbBackup1}, dbPathRest, opt, nil) + require.NoError(t, err, "Restore to Level 1") + + conn, err = sql.Open("firebirdsql", GetTestDSNFromDatabase(dbPathRest)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + + rows, err := conn.Query("select * from test") + require.NoError(t, err, "Query") + require.NotNil(t, rows, "Query") + + var res int + require.True(t, rows.Next(), "Next") + require.NoError(t, rows.Scan(&res), "Scan") + assert.Equal(t, 123, res, "result in restored database should be same as in original") + require.True(t, rows.Next(), "Next") + require.NoError(t, rows.Scan(&res), "Scan") + assert.Equal(t, 456, res, "result in restored database should be same as in original") + + rows.Close() + conn.Close() +} + +func TestNBackupOptions(t *testing.T) { + opts := NewNBackupOptions() + assert.Equal(t, int32(-1), opts.Level) + assert.Equal(t, "", opts.Guid) + assert.Equal(t, int32(0), opts.GetOptionsMask()) + opts = NewNBackupOptions(WithLevel(1), WithGuid("abc"), WithDBTriggers(), WithPlaceRestore(), WithPreserveSequence()) + assert.Equal(t, int32(1), opts.Level) + assert.Equal(t, "abc", opts.Guid) + assert.Equal(t, int32(6), opts.GetOptionsMask()) +} diff --git a/service_manager.go b/service_manager.go new file mode 100644 index 0000000..9950547 --- /dev/null +++ b/service_manager.go @@ -0,0 +1,571 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +import ( + "bytes" + "fmt" + "strings" + "time" +) + +type ServiceManager struct { + wp *wireProtocol + handle int32 +} + +type StatisticsOptions struct { + UserDataPages bool + UserIndexPages bool + OnlyHeaderPages bool + SystemRelationsAndIndexes bool + RecordVersions bool + Tables []string +} + +type StatisticsOption func(*StatisticsOptions) + +type SrvDbInfo struct { + AttachmentsCount int + DatabaseCount int + Databases []string +} + +type ServiceManagerOptions struct { + WireCrypt bool + AuthPlugin string +} + +type ServiceManagerOption func(*ServiceManagerOptions) + +func GetServiceInfoSPBPreamble() []byte { + return []byte{isc_spb_version, isc_spb_current_version} +} + +func GetDefaultStatisticsOptions() StatisticsOptions { + return StatisticsOptions{ + UserDataPages: true, + UserIndexPages: true, + OnlyHeaderPages: false, + SystemRelationsAndIndexes: false, + RecordVersions: false, + Tables: []string{}, + } +} + +func WithUserDataPages() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.UserDataPages = true + } +} + +func WithoutUserDataPages() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.UserDataPages = false + } +} + +func WithUserIndexPages() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.UserIndexPages = true + } +} + +func WithoutIndexPages() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.UserIndexPages = false + } +} + +func WithOnlyHeaderPages() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.OnlyHeaderPages = true + } +} + +func WithoutOnlyHeaderPages() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.OnlyHeaderPages = false + } +} + +func WithSystemRelationsAndIndexes() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.SystemRelationsAndIndexes = true + } +} + +func WithoutSystemRelationsAndIndexes() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.SystemRelationsAndIndexes = false + } +} + +func WithRecordVersions() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.RecordVersions = true + } +} + +func WithoutRecordVersions() StatisticsOption { + return func(opts *StatisticsOptions) { + opts.RecordVersions = false + } +} + +func WithTables(tables []string) StatisticsOption { + return func(opts *StatisticsOptions) { + opts.Tables = tables + } +} + +func NewStatisticsOptions(opts ...StatisticsOption) StatisticsOptions { + res := GetDefaultStatisticsOptions() + for _, opt := range opts { + opt(&res) + } + return res +} + +func GetDefaultServiceManagerOptions() ServiceManagerOptions { + return ServiceManagerOptions{ + WireCrypt: true, + AuthPlugin: "Srp256", + } +} + +func WithWireCrypt() ServiceManagerOption { + return func(opts *ServiceManagerOptions) { + opts.WireCrypt = true + } +} + +func WithoutWireCrypt() ServiceManagerOption { + return func(opts *ServiceManagerOptions) { + opts.WireCrypt = false + } +} + +func WithAuthPlugin(authPlugin string) ServiceManagerOption { + return func(opts *ServiceManagerOptions) { + opts.AuthPlugin = authPlugin + } +} + +func NewServiceManagerOptions(opts ...ServiceManagerOption) ServiceManagerOptions { + res := GetDefaultServiceManagerOptions() + for _, opt := range opts { + opt(&res) + } + return res +} + +func (sm ServiceManagerOptions) WithoutWireCrypt() ServiceManagerOptions { + sm.WireCrypt = false + return sm +} + +func (sm ServiceManagerOptions) WithWireCrypt() ServiceManagerOptions { + sm.WireCrypt = true + return sm +} + +func (sm ServiceManagerOptions) WithAuthPlugin(authPlugin string) ServiceManagerOptions { + sm.AuthPlugin = authPlugin + return sm +} + +func NewServiceManager(addr string, user string, password string, options ServiceManagerOptions) (*ServiceManager, error) { + var err error + var wp *wireProtocol + if !strings.ContainsRune(addr, ':') { + addr += ":3050" + } + if wp, err = newWireProtocol(addr, "", ""); err != nil { + return nil, err + } + + wireCryptStr := "false" + if options.WireCrypt { + wireCryptStr = "true" + } + + var connOptions = map[string]string{ + "auth_plugin_name": options.AuthPlugin, + "wire_crypt": wireCryptStr, + } + + clientPublic, clientSecret := getClientSeed() + if err = wp.opConnect("", user, password, connOptions, clientPublic); err != nil { + return nil, err + } + + if err = wp._parse_connect_response(user, password, connOptions, clientPublic, clientSecret); err != nil { + return nil, err + } + + if err = wp.opServiceAttach(); err != nil { + return nil, err + } + + if wp.dbHandle, _, _, err = wp.opResponse(); err != nil { + return nil, err + } + + manager := &ServiceManager{ + wp: wp, + } + return manager, nil +} + +func (svc *ServiceManager) Close() (err error) { + if err = svc.wp.opServiceDetach(); err != nil { + svc.wp.conn.Close() + return err + } + + if _, _, _, err = svc.wp.opResponse(); err != nil { + svc.wp.conn.Close() + return err + } + + return svc.wp.conn.Close() +} + +func (svc *ServiceManager) ServiceStart(spb []byte) error { + var err error + if err = svc.wp.opServiceStart(spb); err != nil { + return err + } + _, _, _, err = svc.wp.opResponse() + return err +} + +func (svc *ServiceManager) ServiceAttach(spb []byte, verbose chan string) error { + if err := svc.ServiceStart(spb); err != nil { + return err + } + if verbose != nil { + return svc.WaitStrings(verbose) + } else { + return svc.Wait() + } +} + +func (svc *ServiceManager) ServiceAttachBuffer(spb []byte, verbose chan []byte) error { + if err := svc.ServiceStart(spb); err != nil { + return err + } + return svc.WaitBuffer(verbose) +} + +func (svc *ServiceManager) IsRunning() (bool, error) { + res, err := svc.GetServiceInfoInt(isc_info_svc_running) + return res > 0, err +} + +func (svc *ServiceManager) Wait() error { + var ( + err error + running bool + ) + for { + if running, err = svc.IsRunning(); err != nil { + return err + } + if !running { + break + } + time.Sleep(100 * time.Millisecond) + } + return nil +} + +func (svc *ServiceManager) WaitBuffer(stream chan []byte) error { + var ( + err error + buf []byte + cont = true + bufferLength int32 = BUFFER_LEN + ) + for cont { + spb := NewXPBWriterFromBytes(GetServiceInfoSPBPreamble()) + spb.PutByte(isc_info_svc_timeout, 1) + if buf, err = svc.GetServiceInfo(spb.Bytes(), []byte{isc_info_svc_to_eof}, bufferLength); err != nil { + return err + } + switch buf[0] { + case isc_info_svc_to_eof: + dataLen := bytes_to_int16(buf[1:3]) + if dataLen == 0 { + if buf[3] == isc_info_svc_timeout { + break + } else if buf[3] != isc_info_end { + return fmt.Errorf("unexpected end of stream") + } else { + cont = false + break + } + } + stream <- buf[3 : 3+dataLen] + case isc_info_truncated: + bufferLength *= 2 + case isc_info_end: + cont = false + } + } + return nil +} + +func (svc *ServiceManager) WaitStrings(result chan string) error { + var ( + err error + line string + end = false + ) + + for { + if line, end, err = svc.GetString(); err != nil { + return err + } + if end { + return nil + } + result <- line + } +} + +func (svc *ServiceManager) WaitString() (string, error) { + part := make(chan string) + done := make(chan bool) + var err error + var result string + + go func() { + err = svc.WaitStrings(part) + done <- true + }() + + var s string + for cont := true; cont; { + select { + case s = <-part: + result += s + "\n" + case <-done: + cont = false + break + default: + } + } + + if err != nil { + return "", err + } + + return result, nil +} + +func (svc *ServiceManager) GetString() (result string, end bool, err error) { + var buf []byte + if buf, err = svc.GetServiceInfo(GetServiceInfoSPBPreamble(), []byte{isc_info_svc_line}, -1); err != nil { + return "", false, nil + } + if bytes.Compare(buf[:4], []byte{isc_info_svc_line, 0, 0, isc_info_end}) == 0 { + return "", true, nil + } + + return NewXPBReader(buf[1:]).GetString(), false, nil +} + +func (svc *ServiceManager) GetServiceInfo(spb []byte, srb []byte, bufferLength int32) ([]byte, error) { + var buf []byte + var err error + + if err = svc.wp.opServiceInfo(spb, srb, bufferLength); err != nil { + return nil, err + } + + if _, _, buf, err = svc.wp.opResponse(); err != nil { + return nil, err + } + + if len(buf) == 0 { + return nil, fmt.Errorf("response buffer is empty") + } + + if buf[0] != srb[0] { + return nil, fmt.Errorf("wrong item '%d' response buffer", buf[0]) + } + + return buf, nil +} + +func (svc *ServiceManager) GetServiceInfoInt(item byte) (int16, error) { + var buf []byte + var err error + if buf, err = svc.GetServiceInfo(GetServiceInfoSPBPreamble(), []byte{item}, BUFFER_LEN); err != nil { + return 0, err + } + return NewXPBReader(buf[1:]).GetInt16(), nil +} + +func (svc *ServiceManager) GetServiceInfoString(item byte) (string, error) { + var buf []byte + var err error + if buf, err = svc.GetServiceInfo(GetServiceInfoSPBPreamble(), []byte{item}, -1); err != nil { + return "", err + } + return NewXPBReader(buf[1:]).GetString(), nil +} + +func (svc *ServiceManager) GetServerVersionString() (string, error) { + return svc.GetServiceInfoString(isc_info_svc_server_version) +} + +func (svc *ServiceManager) GetServerVersion() (FirebirdVersion, error) { + if ver, err := svc.GetServerVersionString(); err == nil { + return ParseFirebirdVersion(ver), nil + } else { + return FirebirdVersion{}, err + } +} + +func (svc *ServiceManager) GetArchitecture() (string, error) { + return svc.GetServiceInfoString(isc_info_svc_implementation) +} + +func (svc *ServiceManager) GetHomeDir() (string, error) { + return svc.GetServiceInfoString(isc_info_svc_get_env) +} + +func (svc *ServiceManager) GetSecurityDatabasePath() (string, error) { + return svc.GetServiceInfoString(isc_info_svc_user_dbpath) +} + +func (svc *ServiceManager) GetLockFileDir() (string, error) { + return svc.GetServiceInfoString(isc_info_svc_get_env_lock) +} + +func (svc *ServiceManager) GetMsgFileDir() (string, error) { + return svc.GetServiceInfoString(isc_info_svc_get_env_msg) +} + +func (svc *ServiceManager) GetSvrDbInfo() (*SrvDbInfo, error) { + var buf []byte + var err error + + if buf, err = svc.GetServiceInfo(GetServiceInfoSPBPreamble(), []byte{isc_info_svc_svr_db_info}, -1); err != nil { + return &SrvDbInfo{}, err + } + + var attachmentsCount int32 = 0 + var databasesCount int32 = 0 + var databases []string + + srb := NewXPBReader(buf) + have, val := srb.Next() + for ; have && val != isc_info_flag_end; have, val = srb.Next() { + switch val { + case isc_spb_num_att: + attachmentsCount = srb.GetInt32() + case isc_spb_num_db: + databasesCount = srb.GetInt32() + case isc_spb_dbname: + databases = append(databases, srb.GetString()) + } + } + + return &SrvDbInfo{int(attachmentsCount), int(databasesCount), databases}, nil +} + +func (svc *ServiceManager) doGetFbLog() error { + return svc.ServiceStart([]byte{isc_action_svc_get_fb_log}) +} + +func (svc *ServiceManager) GetFbLog(result chan string) error { + if err := svc.doGetFbLog(); err != nil { + return err + } + return svc.WaitStrings(result) +} + +func (svc *ServiceManager) GetFbLogString() (string, error) { + if err := svc.doGetFbLog(); err != nil { + return "", err + } + return svc.WaitString() +} + +func (svc *ServiceManager) doGetDbStats(database string, options StatisticsOptions) error { + var optMask int32 + if options.OnlyHeaderPages { + options.UserDataPages = false + options.UserIndexPages = false + options.SystemRelationsAndIndexes = false + options.RecordVersions = false + } + + if options.UserDataPages { + optMask |= isc_spb_sts_data_pages + } + if options.OnlyHeaderPages { + optMask |= isc_spb_sts_hdr_pages + } + if options.UserIndexPages { + optMask |= isc_spb_sts_idx_pages + } + if options.SystemRelationsAndIndexes { + optMask |= isc_spb_sts_sys_relations + } + if options.RecordVersions { + optMask |= isc_spb_sts_record_versions + } + if options.Tables != nil && len(options.Tables) > 0 { + optMask |= isc_spb_sts_table + } + + spb := NewXPBWriterFromTag(isc_action_svc_db_stats) + spb.PutString(isc_spb_dbname, database) + spb.PutInt32(isc_spb_options, optMask) + + if options.Tables != nil && len(options.Tables) > 0 { + spb.PutString(isc_spb_command_line, strings.Join(options.Tables, " ")) + } + + return svc.ServiceStart(spb.Bytes()) +} + +func (svc *ServiceManager) GetDbStats(database string, options StatisticsOptions, result chan string) error { + if err := svc.doGetDbStats(database, options); err != nil { + return err + } + return svc.WaitStrings(result) +} + +func (svc *ServiceManager) GetDbStatsString(database string, options StatisticsOptions) (string, error) { + if err := svc.doGetDbStats(database, options); err != nil { + return "", err + } + return svc.WaitString() +} diff --git a/service_manager_test.go b/service_manager_test.go new file mode 100644 index 0000000..65eef9c --- /dev/null +++ b/service_manager_test.go @@ -0,0 +1,94 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +import ( + "database/sql" + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestServiceManager_Info(t *testing.T) { + dbPath := GetTestDatabase("test_service_manager_info_") + conn, err := sql.Open("firebirdsql_createdb", GetTestDSNFromDatabase(dbPath)) + require.NoError(t, err, "sql.Open") + require.NotNil(t, conn, "sql.Open") + err = conn.Ping() + require.NoError(t, err, "DB.Ping") + var s string + conn.QueryRow("SELECT rdb$get_context('SYSTEM', 'ENGINE_VERSION') from rdb$database").Scan(&s) + + sm, err := NewServiceManager("localhost:3050", GetTestUser(), GetTestPassword(), GetDefaultServiceManagerOptions()) + require.NoError(t, err, "NewServiceManager") + require.NotNil(t, sm, "NewServiceManager") + defer sm.Close() + + version, err := sm.GetServerVersion() + assert.NoError(t, err, "GetServerVersion") + assert.Equal(t, s, fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Patch)) + + s, err = sm.GetArchitecture() + assert.NoError(t, err, "GetArchitecture") + assert.NotEmpty(t, s, "GetArchitecture") + + s, err = sm.GetHomeDir() + assert.NoError(t, err, "GetHomeDir") + assert.NotEmpty(t, s, "GetHomeDir") + + s, err = sm.GetLockFileDir() + assert.NoError(t, err, "GetLockFileDir") + assert.NotEmpty(t, s, "GetLockFileDir") + + s, err = sm.GetSecurityDatabasePath() + assert.NoError(t, err, "GetSecurityDatabasePath") + assert.NotEmpty(t, s, "GetSecurityDatabasePath") + + dbInfo, err := sm.GetSvrDbInfo() + assert.NotZero(t, dbInfo.DatabaseCount) + found := false + for _, db := range dbInfo.Databases { + if db == dbPath { + found = true + break + } + } + assert.True(t, found, "database found in GetSvrDbInfo") + + s, err = sm.GetFbLogString() + assert.NoError(t, err, "GetFbLogString") + assert.NotEmpty(t, s, "GetFbLogString") + + s, err = sm.GetDbStatsString(dbPath, NewStatisticsOptions(WithOnlyHeaderPages())) + assert.NoError(t, err, "GetDbStatsString") + assert.NotEmpty(t, s, "GetDbStatsString") +} + +func TestServiceManagerOptions(t *testing.T) { + opts := NewServiceManagerOptions() + assert.Equal(t, ServiceManagerOptions{WireCrypt: true, AuthPlugin: "Srp256"}, opts) + opts = NewServiceManagerOptions(WithoutWireCrypt(), WithAuthPlugin("LegacyAuth")) + assert.Equal(t, ServiceManagerOptions{WireCrypt: false, AuthPlugin: "LegacyAuth"}, opts) +} diff --git a/trace_manager.go b/trace_manager.go new file mode 100644 index 0000000..65e8b69 --- /dev/null +++ b/trace_manager.go @@ -0,0 +1,225 @@ +package firebirdsql + +import ( + "fmt" + "regexp" + "strconv" +) + +type TraceManager struct { + connBuilder func() (*ServiceManager, error) +} + +const ( + SessionStopped = iota + SessionRunning + SessionPaused +) + +type TraceSession struct { + connBuilder func() (*ServiceManager, error) + conn *ServiceManager + id int32 + state int +} + +func NewTraceManager(addr string, user string, password string, options ServiceManagerOptions) (*TraceManager, error) { + connBuilder := func() (*ServiceManager, error) { + return NewServiceManager(addr, user, password, options) + } + return &TraceManager{ + connBuilder: connBuilder, + }, nil +} + +func (t *TraceManager) Start(config string) (*TraceSession, error) { + return t.StartWithName("", config) +} + +func (t *TraceManager) StartWithName(name string, config string) (*TraceSession, error) { + var ( + conn *ServiceManager + id int64 + err error + ) + if conn, err = t.connBuilder(); err != nil { + return nil, err + } + + var res string + var spb = NewXPBWriterFromTag(isc_action_svc_trace_start) + + if len(name) > 0 { + spb.PutString(isc_spb_trc_name, name) + } + + spb.PutString(isc_spb_trc_cfg, config) + + if err = conn.ServiceStart(spb.Bytes()); err != nil { + return nil, err + } + if res, _, err = conn.GetString(); err != nil { + return nil, err + } + re := regexp.MustCompile(`Trace session ID (\d+) started`) + match := re.FindStringSubmatch(res) + if len(match) == 0 { + _ = conn.Close() + return nil, fmt.Errorf("unable to start trace session: %s", res) + } + if id, err = strconv.ParseInt(match[1], 10, 32); err != nil { + return nil, err + } + + return &TraceSession{ + connBuilder: t.connBuilder, + conn: conn, + id: int32(id), + state: SessionRunning, + }, nil +} + +func (t *TraceManager) List() (string, error) { + var ( + err error + line, res string + end = false + conn *ServiceManager + ) + if conn, err = t.connBuilder(); err != nil { + return "", nil + } + defer func(conn *ServiceManager) { + _ = conn.Close() + }(conn) + + if err = conn.ServiceStart([]byte{isc_action_svc_trace_list}); err != nil { + return "", err + } + + for { + if line, end, err = conn.GetString(); err != nil { + return "", nil + } + if end { + return res, nil + } + res += line + "\n" + } +} + +func (ts *TraceSession) Close() (err error) { + if ts.state != SessionStopped { + if err = ts.Stop(); err != nil { + return err + } + } + if err = ts.conn.Close(); err != nil { + return err + } + ts.conn = nil + return nil +} + +func (ts *TraceSession) Stop() (err error) { + if ts.state == SessionStopped { + return fmt.Errorf("session already stopped") + } + var auxConn *ServiceManager + if auxConn, err = ts.connBuilder(); err != nil { + return + } + defer func(auxConn *ServiceManager) { + _ = auxConn.Close() + }(auxConn) + + var res string + spb := NewXPBWriterFromTag(isc_action_svc_trace_stop) + spb.PutInt32(isc_spb_trc_id, ts.id) + + if err = auxConn.ServiceStart(spb.Bytes()); err != nil { + return err + } + if res, _, err = auxConn.GetString(); err != nil { + return err + } + re := regexp.MustCompile(`Trace session ID (\d+) stopped`) + match := re.FindStringSubmatch(res) + if len(match) == 0 { + return fmt.Errorf("unable to stop trace session: %s", res) + } + ts.state = SessionStopped + return nil +} + +func (ts *TraceSession) Pause() (err error) { + if ts.state != SessionRunning { + return fmt.Errorf("session not running") + } + + var auxConn *ServiceManager + if auxConn, err = ts.connBuilder(); err != nil { + return + } + defer func(auxConn *ServiceManager) { + _ = auxConn.Close() + }(auxConn) + + var res string + spb := NewXPBWriterFromTag(isc_action_svc_trace_suspend) + spb.PutInt32(isc_spb_trc_id, ts.id) + + if err = auxConn.ServiceStart(spb.Bytes()); err != nil { + return err + } + if res, _, err = auxConn.GetString(); err != nil { + return err + } + re := regexp.MustCompile(`Trace session ID (\d+) paused`) + match := re.FindStringSubmatch(res) + if len(match) == 0 { + return fmt.Errorf("unable to pause trace session: %s", res) + } + ts.state = SessionPaused + return nil +} + +func (ts *TraceSession) Resume() (err error) { + if ts.state == SessionPaused { + return fmt.Errorf("session not paused") + } + + var auxConn *ServiceManager + if auxConn, err = ts.connBuilder(); err != nil { + return + } + defer func(auxConn *ServiceManager) { + _ = auxConn.Close() + }(auxConn) + + var res string + spb := NewXPBWriterFromTag(isc_action_svc_trace_resume) + spb.PutInt32(isc_spb_trc_id, ts.id) + + if err = auxConn.ServiceStart(spb.Bytes()); err != nil { + return err + } + if res, _, err = auxConn.GetString(); err != nil { + return err + } + re := regexp.MustCompile(`Trace session ID (\d+) resumed`) + match := re.FindStringSubmatch(res) + if len(match) == 0 { + return fmt.Errorf("unable to resume trace session: %s", res) + } + ts.state = SessionRunning + return nil +} + +func (ts *TraceSession) Wait() (err error) { + return ts.conn.Wait() +} + +func (ts *TraceSession) WaitStrings(result chan string) (err error) { + return ts.conn.WaitStrings(result) +} diff --git a/user_manager.go b/user_manager.go new file mode 100644 index 0000000..4d9e50f --- /dev/null +++ b/user_manager.go @@ -0,0 +1,297 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +type User struct { + Username *string + Password *string + FirstName *string + MiddleName *string + LastName *string + UserId int32 + GroupId int32 + Admin *bool +} + +type UserManager struct { + sm *ServiceManager + securityDb string +} + +type UserManagerOptions struct { + SecurityDB string +} + +type UserManagerOption func(*UserManagerOptions) + +func WithSecurityDB(securityDB string) UserManagerOption { + return func(opts *UserManagerOptions) { + opts.SecurityDB = securityDB + } +} + +func GetDefaultUserManagerOptions() UserManagerOptions { + return UserManagerOptions{ + SecurityDB: "", + } +} + +func NewUserManagerOptions(opts ...UserManagerOption) UserManagerOptions { + res := GetDefaultUserManagerOptions() + for _, opt := range opts { + opt(&res) + } + return res +} + +type UserOption func(*User) + +func WithUsername(username string) UserOption { + return func(opts *User) { + opts.Username = &username + } +} + +func WithPassword(password string) UserOption { + return func(opts *User) { + opts.Password = &password + } +} + +func WithFirstName(firstname string) UserOption { + return func(opts *User) { + opts.FirstName = &firstname + } +} + +func WithMiddleName(middlename string) UserOption { + return func(opts *User) { + opts.MiddleName = &middlename + } +} + +func WithLastName(lastname string) UserOption { + return func(opts *User) { + opts.LastName = &lastname + } +} + +func WithUserId(userId int32) UserOption { + return func(opts *User) { + opts.UserId = userId + } +} + +func WithGroupId(groupId int32) UserOption { + return func(opts *User) { + opts.GroupId = groupId + } +} + +func WithAdmin() UserOption { + return func(opts *User) { + res := true + opts.Admin = &res + } +} + +func WithoutAdmin() UserOption { + return func(opts *User) { + res := false + opts.Admin = &res + } +} + +func NewUser(opts ...UserOption) User { + res := User{ + UserId: -1, + GroupId: -1, + } + for _, opt := range opts { + opt(&res) + } + return res +} + +func (u *User) GetSpb() []byte { + srb := NewXPBWriter() + if u.Username != nil { + srb.PutString(isc_spb_sec_username, *u.Username) + } + if u.Password != nil { + srb.PutString(isc_spb_sec_password, *u.Password) + } + if u.FirstName != nil { + srb.PutString(isc_spb_sec_firstname, *u.FirstName) + } + if u.MiddleName != nil { + srb.PutString(isc_spb_sec_middlename, *u.MiddleName) + } + if u.LastName != nil { + srb.PutString(isc_spb_sec_lastname, *u.LastName) + } + if u.UserId != -1 { + srb.PutInt32(isc_spb_sec_userid, u.UserId) + } + if u.GroupId != -1 { + srb.PutInt32(isc_spb_sec_groupid, u.GroupId) + } + if u.Admin != nil { + if *u.Admin { + srb.PutInt32(isc_spb_sec_admin, 1) + } else { + srb.PutInt32(isc_spb_sec_admin, 0) + } + } + return srb.Bytes() +} + +func NewUserManager(addr string, user string, password string, smo ServiceManagerOptions, umo UserManagerOptions) (*UserManager, error) { + var ( + sm *ServiceManager + err error + ) + + if sm, err = NewServiceManager(addr, user, password, smo); err != nil { + return nil, err + } + return &UserManager{ + sm, + umo.SecurityDB, + }, nil +} + +func (um *UserManager) Close() error { + return um.sm.Close() +} + +func (um *UserManager) userAction(action byte, user *User) error { + spb := NewXPBWriterFromTag(action) + if user != nil { + spb.PutBytes(user.GetSpb()) + } + if um.securityDb != "" { + spb.PutString(isc_spb_dbname, um.securityDb) + } + return um.sm.ServiceStart(spb.Bytes()) +} + +func (um *UserManager) AddUser(user User) error { + err := um.userAction(isc_action_svc_add_user, &user) + return err +} + +func (um *UserManager) DeleteUser(user User) error { + del := NewUser(WithUsername(*user.Username)) + err := um.userAction(isc_action_svc_delete_user, &del) + return err +} + +func (um *UserManager) ModifyUser(user User) error { + err := um.userAction(isc_action_svc_modify_user, &user) + return err +} + +func (um *UserManager) GetUsers() ([]User, error) { + var ( + err error + buf []byte + resChan = make(chan []byte) + done = make(chan bool) + cont = true + users []User + ) + if err = um.userAction(isc_action_svc_display_user_adm, nil); err != nil { + return nil, err + } + + go func() { + err = um.sm.WaitBuffer(resChan) + done <- true + }() + + for cont { + select { + case buf = <-resChan: + srb := NewXPBReader(buf) + var ( + user *User + have bool + val byte + ) + for { + if have, val = srb.Next(); !have { + break + } + switch val { + case isc_spb_sec_username: + if user != nil { + users = append(users, *user) + } + u := NewUser(WithUsername(srb.GetString())) + user = &u + case isc_spb_sec_firstname: + s := srb.GetString() + user.FirstName = &s + case isc_spb_sec_middlename: + s := srb.GetString() + user.MiddleName = &s + case isc_spb_sec_lastname: + s := srb.GetString() + user.LastName = &s + case isc_spb_sec_userid: + user.UserId = srb.GetInt32() + case isc_spb_sec_groupid: + user.GroupId = srb.GetInt32() + case isc_spb_sec_admin: + a := srb.GetInt32() > 0 + user.Admin = &a + } + } + if user != nil { + users = append(users, *user) + } + case <-done: + cont = false + } + } + + return users, err +} + +func (um *UserManager) adminRoleMappingAction(action byte) error { + spb := NewXPBWriterFromTag(action) + if um.securityDb != "" { + spb.PutString(isc_spb_dbname, um.securityDb) + } + return um.sm.ServiceStart(spb.Bytes()) +} + +func (um *UserManager) SetAdminRoleMapping() error { + return um.adminRoleMappingAction(isc_action_svc_set_mapping) +} + +func (um *UserManager) DropAdminRoleMapping() error { + return um.adminRoleMappingAction(isc_action_svc_drop_mapping) +} diff --git a/user_manager_test.go b/user_manager_test.go new file mode 100644 index 0000000..5730022 --- /dev/null +++ b/user_manager_test.go @@ -0,0 +1,133 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +import ( + "database/sql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestUserManager(t *testing.T) { + dbPath := GetTestDatabase("test_user_manager_") + conn, err := sql.Open("firebirdsql_createdb", GetTestDSNFromDatabase(dbPath)) + require.NoError(t, err) + require.NotNil(t, conn) + defer conn.Close() + err = conn.Ping() + require.NoError(t, err) + + um, err := NewUserManager("localhost:3050", GetTestUser(), GetTestPassword(), NewServiceManagerOptions(), NewUserManagerOptions()) + require.NoError(t, err, "NewUserManager") + require.NotNil(t, um, "NewUserManager") + defer um.Close() + + users, err := um.GetUsers() + assert.NoError(t, err, "GetUsers") + + haveSysdba := false + haveTest := false + for _, user := range users { + if *user.Username == "SYSDBA" { + haveSysdba = true + } + if *user.Username == "TEST" { + haveTest = true + assert.False(t, *user.Admin, "admin flag") + } + } + assert.True(t, haveSysdba, "sysdba found") + assert.False(t, haveTest, "test user not found") + + err = um.AddUser(NewUser(WithUsername("test"), WithPassword("test"), WithFirstName("xxx"))) + assert.NoError(t, err, "AddUser") + + defer func() { + err = um.DeleteUser(NewUser(WithUsername("test"))) + assert.NoError(t, err, "DeleteUser") + }() + + conn, err = sql.Open("firebirdsql", GetTestDSNFromDatabaseUserPassword(dbPath, "test", "test")) + require.NoError(t, err) + require.NotNil(t, conn) + assert.NoError(t, conn.Ping()) + conn.Close() + + err = um.ModifyUser(NewUser(WithUsername("test"), WithLastName("testlastname"), WithPassword("zzz"), WithUserId(1), WithAdmin())) + assert.NoError(t, err, "ModifyUser") + + conn, err = sql.Open("firebirdsql", GetTestDSNFromDatabaseUserPassword(dbPath, "test", "zzz")) + require.NoError(t, err) + require.NotNil(t, conn) + assert.NoError(t, conn.Ping()) + conn.Close() + + users, err = um.GetUsers() + assert.NoError(t, err, "GetUsers") + + haveSysdba = false + haveTest = false + for _, user := range users { + if *user.Username == "SYSDBA" { + haveSysdba = true + } + if *user.Username == "TEST" { + haveTest = true + assert.NotNil(t, user.LastName) + assert.Equal(t, "testlastname", *user.LastName) + assert.True(t, *user.Admin, "admin flag") + } + } + assert.True(t, haveSysdba, "sysdba found") + assert.True(t, haveTest, "test user found") + + assert.NoError(t, um.SetAdminRoleMapping()) + assert.NoError(t, um.DropAdminRoleMapping()) +} + +func TestUserManagerOptions(t *testing.T) { + opts := NewUserManagerOptions() + assert.Equal(t, UserManagerOptions{SecurityDB: ""}, opts) + opts = NewUserManagerOptions(WithSecurityDB("secdb")) + assert.Equal(t, UserManagerOptions{SecurityDB: "secdb"}, opts) +} + +func TestUserOptions(t *testing.T) { + user := NewUser() + assert.Equal(t, User{Username: nil, Password: nil, FirstName: nil, MiddleName: nil, LastName: nil, UserId: -1, GroupId: -1, Admin: nil}, user) + user = NewUser(WithUsername("test"), WithPassword("pwd"), WithFirstName("qqq"), WithMiddleName("www"), WithLastName("eee"), WithUserId(100), WithGroupId(200), WithAdmin()) + assert.Equal(t, "test", *user.Username) + assert.Equal(t, "pwd", *user.Password) + assert.Equal(t, "qqq", *user.FirstName) + assert.Equal(t, "www", *user.MiddleName) + assert.Equal(t, "eee", *user.LastName) + assert.Equal(t, int32(100), user.UserId) + assert.Equal(t, int32(200), user.GroupId) + require.NotNil(t, user.Admin) + assert.True(t, *user.Admin) + user = NewUser(WithoutAdmin()) + require.NotNil(t, user.Admin) + assert.False(t, *user.Admin) +} diff --git a/utils.go b/utils.go index 9a36400..9eb8206 100644 --- a/utils.go +++ b/utils.go @@ -46,6 +46,20 @@ func int32_to_bytes(i32 int32) []byte { return bs } +func int64_to_bytes(i64 int64) []byte { + bs := []byte{ + byte(i64 & 0xFF), + byte(i64 >> 8 & 0xFF), + byte(i64 >> 16 & 0xFF), + byte(i64 >> 24 & 0xFF), + byte(i64 >> 32 & 0xFF), + byte(i64 >> 40 & 0xFF), + byte(i64 >> 40 & 0xFF), + byte(i64 >> 56 & 0xFF), + } + return bs +} + func bint64_to_bytes(i64 int64) []byte { bs := []byte{ byte(i64 >> 56 & 0xFF), @@ -241,3 +255,7 @@ func convertToBool(s string, defaultValue bool) bool { } return v } + +func fitsUint32(val int64) bool { + return val >= 0 && val <= 0xffffffff +} diff --git a/wireprotocol.go b/wireprotocol.go index 82ee16d..889751b 100644 --- a/wireprotocol.go +++ b/wireprotocol.go @@ -462,6 +462,8 @@ func (p *wireProtocol) _parse_connect_response(user string, password string, opt p.protocolVersion = int32(b[3]) p.acceptArchitecture = bytes_to_bint32(b[4:8]) p.acceptType = bytes_to_bint32(b[8:12]) + p.user = user + p.password = password if opcode == op_cond_accept || opcode == op_accept_data { var readLength, ln int @@ -1619,3 +1621,61 @@ func (p *wireProtocol) encodeString(str string) string { return str // If the specified charset is not supported, return the input string without any modification or encoding. } } + +func (p *wireProtocol) opServiceAttach() error { + p.debugPrint("opServiceAttach()") + p.packInt(op_service_attach) + p.packInt(0) + p.packString("service_mgr") + + userBytes := bytes.NewBufferString(p.user).Bytes() + passwordBytes := bytes.NewBufferString(p.password).Bytes() + spb := bytes.Join([][]byte{ + {isc_spb_version, isc_spb_current_version}, + {isc_spb_user_name, byte(len(userBytes))}, userBytes, + {isc_spb_password, byte(len(passwordBytes))}, passwordBytes, + {isc_spb_utf8_filename, 1, 1}, + }, nil) + if p.authData != nil { + specificAuthData := bytes.NewBufferString(hex.EncodeToString(p.authData)).Bytes() + spb = bytes.Join([][]byte{ + spb, + {isc_dpb_specific_auth_data, byte(len(specificAuthData))}, specificAuthData}, nil) + } + p.packBytes(spb) + _, err := p.sendPackets() + return err +} + +func (p *wireProtocol) opServiceDetach() error { + p.debugPrint("opServiceDetach()") + p.packInt(op_service_detach) + p.packInt(p.dbHandle) + _, err := p.sendPackets() + return err +} + +func (p *wireProtocol) opServiceInfo(spb []byte, srb []byte, bufferLength int32) error { + p.debugPrint("opServiceInfo(%v, %v, %v)", spb, srb, bufferLength) + if bufferLength <= 0 { + bufferLength = BUFFER_LEN + } + p.packInt(op_service_info) + p.packInt(p.dbHandle) + p.packInt(0) + p.packBytes(spb) + p.packBytes(srb) + p.packInt(bufferLength) + _, err := p.sendPackets() + return err +} + +func (p *wireProtocol) opServiceStart(spb []byte) error { + p.debugPrint("opServiceStart(%v)", spb) + p.packInt(op_service_start) + p.packInt(p.dbHandle) + p.packInt(0) + p.packBytes(spb) + _, err := p.sendPackets() + return err +} diff --git a/xpb.go b/xpb.go new file mode 100644 index 0000000..4d580a0 --- /dev/null +++ b/xpb.go @@ -0,0 +1,154 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +const xpbPreallocBufSize = 16 + +type XPBReader struct { + buf []byte + pos int +} + +type XPBWriter struct { + buf []byte +} + +func NewXPBReader(buf []byte) *XPBReader { + return &XPBReader{buf, 0} +} + +func (pb *XPBReader) Next() (have bool, value byte) { + if pb.End() { + return false, 0 + } + b := pb.buf[pb.pos] + pb.pos++ + return true, b +} + +func (pb *XPBReader) Skip(count int) { + pb.pos += count +} + +func (pb *XPBReader) End() bool { + return pb.pos >= len(pb.buf) +} + +func (pb *XPBReader) Get() byte { + b := pb.buf[pb.pos] + return b +} + +func (pb *XPBReader) GetString() string { + l := int(pb.GetInt16()) + s := bytes_to_str(pb.buf[pb.pos : pb.pos+l]) + pb.pos += l + return s +} + +func (pb *XPBReader) GetInt16() int16 { + r := bytes_to_int16(pb.buf[pb.pos : pb.pos+2]) + pb.pos += 2 + return r +} + +func (pb *XPBReader) GetInt32() int32 { + r := bytes_to_int32(pb.buf[pb.pos : pb.pos+4]) + pb.pos += 4 + return r +} + +func (pb *XPBReader) GetInt64() int64 { + r := bytes_to_int64(pb.buf[pb.pos : pb.pos+8]) + pb.pos += 8 + return r +} + +func (pb *XPBReader) Reset() { + pb.pos = 0 +} + +func NewXPBWriter() *XPBWriter { + return &XPBWriter{ + buf: make([]byte, 0, xpbPreallocBufSize), + } +} + +func NewXPBWriterFromTag(tag byte) *XPBWriter { + return NewXPBWriter().PutTag(tag) +} + +func NewXPBWriterFromBytes(bytes []byte) *XPBWriter { + return NewXPBWriter().PutBytes(bytes) +} + +func (pb *XPBWriter) PutTag(tag byte) *XPBWriter { + pb.buf = append(pb.buf, []byte{tag}...) + return pb +} + +func (pb *XPBWriter) PutByte(tag byte, val byte) *XPBWriter { + pb.buf = append(pb.buf, []byte{tag, val}...) + return pb +} + +func (pb *XPBWriter) PutInt16(tag byte, val int16) *XPBWriter { + pb.buf = append(pb.buf, []byte{tag}...) + pb.buf = append(pb.buf, int16_to_bytes(val)...) + return pb +} + +func (pb *XPBWriter) PutInt32(tag byte, val int32) *XPBWriter { + pb.buf = append(pb.buf, []byte{tag}...) + pb.buf = append(pb.buf, int32_to_bytes(val)...) + return pb +} + +func (pb *XPBWriter) PutInt64(tag byte, val int64) *XPBWriter { + pb.buf = append(pb.buf, []byte{tag}...) + pb.buf = append(pb.buf, int64_to_bytes(val)...) + return pb +} + +func (pb *XPBWriter) PutString(tag byte, val string) *XPBWriter { + strBytes := str_to_bytes(val) + pb.buf = append(pb.buf, []byte{tag}...) + pb.buf = append(pb.buf, int16_to_bytes(int16(len(strBytes)))...) + pb.buf = append(pb.buf, strBytes...) + return pb +} + +func (pb *XPBWriter) PutBytes(bytes []byte) *XPBWriter { + pb.buf = append(pb.buf, bytes...) + return pb +} + +func (pb *XPBWriter) Bytes() []byte { + return pb.buf +} + +func (pb *XPBWriter) Reset() *XPBWriter { + pb.buf = make([]byte, 0, xpbPreallocBufSize) + return pb +} diff --git a/xpb_test.go b/xpb_test.go new file mode 100644 index 0000000..1476ea6 --- /dev/null +++ b/xpb_test.go @@ -0,0 +1,181 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2023-2024 Artyom Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*******************************************************************************/ + +package firebirdsql + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNewXPBReader(t *testing.T) { + assert.NotNil(t, NewXPBReader(nil)) + assert.NotNil(t, NewXPBReader([]byte{1, 2, 3})) +} + +func TestXPBReaderEnd(t *testing.T) { + xpb := NewXPBReader(nil) + require.NotNil(t, xpb) + assert.True(t, xpb.End()) + xpb = NewXPBReader([]byte{1, 2, 3}) + require.NotNil(t, xpb) + assert.False(t, xpb.End()) +} + +func TestXPBReader_GetInt16(t *testing.T) { + xpb := NewXPBReader([]byte{2, 2}) + require.NotNil(t, xpb) + assert.Equal(t, int16(514), xpb.GetInt16()) + require.True(t, xpb.End()) +} + +func TestXPBReader_GetInt32(t *testing.T) { + xpb := NewXPBReader([]byte{1, 2, 3, 4}) + require.NotNil(t, xpb) + assert.Equal(t, int32(67305985), xpb.GetInt32()) + require.True(t, xpb.End()) +} + +func TestXPBReader_GetString(t *testing.T) { + xpb := NewXPBReader([]byte{4, 0, 't', 'e', 's', 't'}) + require.NotNil(t, xpb) + assert.Equal(t, "test", xpb.GetString()) + require.True(t, xpb.End()) +} + +func TestXPBReader_GetMixed(t *testing.T) { + xpb := NewXPBReader([]byte{4, 0, 't', 'e', 's', 't', 1, 1, 2, 2, 2, 2}) + require.NotNil(t, xpb) + assert.Equal(t, "test", xpb.GetString()) + assert.Equal(t, int16(257), xpb.GetInt16()) + assert.Equal(t, int32(33686018), xpb.GetInt32()) + require.True(t, xpb.End()) +} + +func TestXPBReader_Next(t *testing.T) { + xpb := NewXPBReader([]byte{1, 2, 4, 0, 't', 'e', 's', 't'}) + require.NotNil(t, xpb) + have, val := xpb.Next() + assert.True(t, have) + assert.Equal(t, byte(1), val) + have, val = xpb.Next() + assert.True(t, have) + assert.Equal(t, byte(2), val) + assert.Equal(t, "test", xpb.GetString()) + have, _ = xpb.Next() + assert.False(t, have) + require.True(t, xpb.End()) +} + +func TestXPBReader_Get(t *testing.T) { + xpb := NewXPBReader([]byte{3, 4}) + require.NotNil(t, xpb) + v1 := xpb.Get() + _, v2 := xpb.Next() + assert.Equal(t, byte(3), v1) + assert.Equal(t, byte(3), v2) + v1 = xpb.Get() + _, v2 = xpb.Next() + assert.Equal(t, byte(4), v1) + assert.Equal(t, byte(4), v2) +} + +func TestXPBReader_Reset(t *testing.T) { + xpb := NewXPBReader([]byte{4, 0, 't', 'e', 's', 't'}) + require.NotNil(t, xpb) + assert.Equal(t, "test", xpb.GetString()) + require.True(t, xpb.End()) + xpb.Reset() + require.False(t, xpb.End()) + assert.Equal(t, "test", xpb.GetString()) +} + +func TestNewXPBWriter(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + assert.Equal(t, []byte{}, w.Bytes()) +} + +func TestNewXPBWriterFromTag(t *testing.T) { + w := NewXPBWriterFromTag(1) + require.NotNil(t, w) + assert.Equal(t, []byte{1}, w.Bytes()) +} + +func TestNewXPBWriterFromBytes(t *testing.T) { + w := NewXPBWriterFromBytes([]byte{1, 2, 3}) + require.NotNil(t, w) + assert.Equal(t, []byte{1, 2, 3}, w.Bytes()) +} + +func TestXPBWriter_PutTag(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + w.PutTag(5) + assert.Equal(t, []byte{5}, w.Bytes()) +} + +func TestXPBWriter_PutByte(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + w.PutByte(5, 6) + assert.Equal(t, []byte{5, 6}, w.Bytes()) +} + +func TestXPBWriter_PutInt16(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + w.PutInt16(7, 20000) + assert.Equal(t, []byte{7, 0x20, 0x4e}, w.Bytes()) +} + +func TestXPBWriter_PutInt32(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + w.PutInt32(7, 200000000) + assert.Equal(t, []byte{7, 0x0, 0xc2, 0xeb, 0xb}, w.Bytes()) +} + +func TestXPBWriter_PutString(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + w.PutString(8, "test") + assert.Equal(t, []byte{8, 4, 0, 't', 'e', 's', 't'}, w.Bytes()) +} + +func TestXPBWriter_PutMixed(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + w.PutTag(1).PutByte(2, 3).PutInt16(4, 5).PutInt32(6, 7).PutString(8, "test").PutTag(9) + assert.Equal(t, []byte{1, 2, 3, 4, 5, 0, 6, 7, 0, 0, 0, 8, 4, 0, 't', 'e', 's', 't', 9}, w.Bytes()) +} + +func TestXPBWriter_Reset(t *testing.T) { + w := NewXPBWriter() + require.NotNil(t, w) + w.PutString(1, "test") + assert.Equal(t, []byte{1, 4, 0, 't', 'e', 's', 't'}, w.Bytes()) + w.Reset() + assert.Equal(t, []byte{}, w.Bytes()) +}