Skip to content

Commit

Permalink
Track the full paths of attachments in the Attachment object
Browse files Browse the repository at this point in the history
  • Loading branch information
tagatac committed Nov 17, 2024
1 parent f767ab8 commit 9b074a5
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 91 deletions.
1 change: 1 addition & 0 deletions chatdb/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type Attachment struct {
ID int
Filename string
Filepath string
MIMEType string
TransferName string
}
Expand Down
13 changes: 9 additions & 4 deletions internal/bagoup/bagoup.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,17 @@ func (cfg *configuration) validatePaths() error {
if err := cfg.OS.FileAccess(cfg.Options.DBPath); err != nil {
return errors.Wrapf(err, "test DB file %q - FIX: %s", cfg.Options.DBPath, _readmeURL)
}
if ok, err := cfg.OS.FileExist(cfg.Options.ExportPath); err != nil {
return errors.Wrapf(err, "check export path %q", cfg.Options.ExportPath)
var err error
var exportPathAbs string
if exportPathAbs, err = filepath.Abs(cfg.Options.ExportPath); err != nil {
return errors.Wrapf(err, "convert export path %q to an absolute path", cfg.Options.ExportPath)
}
cfg.Options.ExportPath = exportPathAbs
if ok, err := cfg.OS.FileExist(exportPathAbs); err != nil {
return errors.Wrapf(err, "check export path %q", exportPathAbs)
} else if ok {
return fmt.Errorf("export folder %q already exists - FIX: move it or specify a different export path with the --export-path option", cfg.Options.ExportPath)
return fmt.Errorf("export folder %q already exists - FIX: move it or specify a different export path with the --export-path option", exportPathAbs)
}
var err error
var attPathAbs string
if attPathAbs, err = filepath.Abs(cfg.Options.AttachmentsPath); err != nil {
return errors.Wrapf(err, "convert attachments path %q to an absolute path", cfg.Options.AttachmentsPath)
Expand Down
109 changes: 57 additions & 52 deletions internal/bagoup/bagoup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package bagoup

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
Expand All @@ -29,6 +30,10 @@ func TestBagoup(t *testing.T) {
SelfHandle: "Me",
AttachmentsPath: "/",
}
exportPathAbs := filepath.Join(wd, "messages-export")
logDirAbs := filepath.Join(exportPathAbs, ".bagoup")
logFileAbs := filepath.Join(logDirAbs, "out.log")
tildeexpansionAbs := filepath.Join(exportPathAbs, PreservedPathDir, PreservedPathTildeExpansionFile)
tenDotTwelve := "10.12"
tenDotTenDotTenDotTen := "10.10.10.10"
contactsPath := "contacts.vcf"
Expand All @@ -48,9 +53,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, ptMock *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
dbMock.EXPECT().GetHandleMap(nil),
Expand All @@ -72,9 +77,9 @@ func TestBagoup(t *testing.T) {
gomock.InOrder(
osMock.EXPECT().ReadFile("testrelativepath/.tildeexpansion"),
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
dbMock.EXPECT().GetHandleMap(nil),
Expand Down Expand Up @@ -111,9 +116,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, _ *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(nil, errors.New("this is an exec error")),
)
},
Expand All @@ -125,30 +130,30 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, _ *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export").Return(true, nil),
osMock.EXPECT().FileExist(exportPathAbs).Return(true, nil),
)
},
wantErr: `export folder "messages-export" already exists - FIX: move it or specify a different export path with the --export-path option`,
wantErr: fmt.Sprintf(`export folder %q already exists - FIX: move it or specify a different export path with the --export-path option`, exportPathAbs),
},
{
msg: "error checking export path",
opts: defaultOpts,
setupMocks: func(osMock *mock_opsys.MockOS, _ *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export").Return(false, errors.New("this is a stat error")),
osMock.EXPECT().FileExist(exportPathAbs).Return(false, errors.New("this is a stat error")),
)
},
wantErr: `check export path "messages-export": this is a stat error`,
wantErr: fmt.Sprintf(`check export path %q: this is a stat error`, exportPathAbs),
},
{
msg: "error creating log directory",
opts: defaultOpts,
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm).Return(errors.New("this is a permissions error")),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm).Return(errors.New("this is a permissions error")),
)
},
wantErr: "make log directory: this is a permissions error",
Expand All @@ -159,9 +164,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, errors.New("this is a permissions error")),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, errors.New("this is a permissions error")),
)
},
wantErr: "create log file: this is a permissions error",
Expand All @@ -178,9 +183,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, ptMock *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
dbMock.EXPECT().Init(semver.MustParse("10.12")),
dbMock.EXPECT().GetHandleMap(nil),
dbMock.EXPECT().GetAttachmentPaths(ptMock),
Expand All @@ -201,9 +206,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, _ *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
)
},
wantErr: `parse Mac OS version "10.10.10.10": Invalid Semantic Version`,
Expand All @@ -220,9 +225,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, ptMock *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
osMock.EXPECT().GetContactMap("contacts.vcf"),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
Expand All @@ -245,9 +250,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, _ *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
osMock.EXPECT().GetContactMap("contacts.vcf").Return(nil, errors.New("this is an os error")),
)
Expand All @@ -260,9 +265,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")).Return(errors.New("this is a DB error")),
)
Expand All @@ -275,9 +280,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, _ *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
dbMock.EXPECT().GetHandleMap(nil).Return(nil, errors.New("this is a DB error")),
Expand All @@ -291,9 +296,9 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, ptMock *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
dbMock.EXPECT().GetHandleMap(nil),
Expand All @@ -317,16 +322,16 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, ptMock *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
dbMock.EXPECT().GetHandleMap(nil),
dbMock.EXPECT().GetAttachmentPaths(ptMock),
dbMock.EXPECT().GetChats(nil),
ptMock.EXPECT().GetHomeDir(),
osMock.EXPECT().Create("messages-export/bagoup-attachments/.tildeexpansion").Return(afero.NewMemMapFs().Create("dummy")),
osMock.EXPECT().Create(tildeexpansionAbs).Return(afero.NewMemMapFs().Create("dummy")),
osMock.EXPECT().RmTempDir().Times(2),
)
},
Expand All @@ -344,16 +349,16 @@ func TestBagoup(t *testing.T) {
setupMocks: func(osMock *mock_opsys.MockOS, dbMock *mock_chatdb.MockChatDB, ptMock *mock_pathtools.MockPathTools) {
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
dbMock.EXPECT().GetHandleMap(nil),
dbMock.EXPECT().GetAttachmentPaths(ptMock),
dbMock.EXPECT().GetChats(nil),
ptMock.EXPECT().GetHomeDir(),
osMock.EXPECT().Create("messages-export/bagoup-attachments/.tildeexpansion").Return(nil, errors.New("this is a permissions error")),
osMock.EXPECT().Create(tildeexpansionAbs).Return(nil, errors.New("this is a permissions error")),
osMock.EXPECT().RmTempDir(),
)
},
Expand All @@ -376,16 +381,16 @@ func TestBagoup(t *testing.T) {
rofs := afero.NewReadOnlyFs(rwfs)
gomock.InOrder(
osMock.EXPECT().FileAccess("~/Library/Messages/chat.db"),
osMock.EXPECT().FileExist("messages-export"),
osMock.EXPECT().MkdirAll("messages-export/.bagoup", os.ModePerm),
osMock.EXPECT().Create("messages-export/.bagoup/out.log").Return(devnull, nil),
osMock.EXPECT().FileExist(exportPathAbs),
osMock.EXPECT().MkdirAll(logDirAbs, os.ModePerm),
osMock.EXPECT().Create(logFileAbs).Return(devnull, nil),
osMock.EXPECT().GetMacOSVersion().Return(semver.MustParse("12.4"), nil),
dbMock.EXPECT().Init(semver.MustParse("12.4")),
dbMock.EXPECT().GetHandleMap(nil),
dbMock.EXPECT().GetAttachmentPaths(ptMock),
dbMock.EXPECT().GetChats(nil),
ptMock.EXPECT().GetHomeDir(),
osMock.EXPECT().Create("messages-export/bagoup-attachments/.tildeexpansion").Return(rofs.Open("dummy")),
osMock.EXPECT().Create(tildeexpansionAbs).Return(rofs.Open("dummy")),
osMock.EXPECT().RmTempDir(),
)
},
Expand All @@ -407,7 +412,7 @@ func TestBagoup(t *testing.T) {
osMock,
dbMock,
ptMock,
"messages-export/.bagoup",
logDirAbs,
time.Now(),
"",
)
Expand Down
35 changes: 16 additions & 19 deletions internal/bagoup/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,16 @@ func (cfg *configuration) handleAttachments(outFile opsys.OutFile, msgID int, at
return nil
}
for _, att := range msgPaths {
attPath, mimeType, transferName := att.Filename, att.MIMEType, att.TransferName
err := cfg.validateAttachmentPath(attPath)
att.Filepath = filepath.Join(cfg.Options.AttachmentsPath, att.Filename)
err := cfg.validateAttachmentPath(att)
if _, ok := err.(errorMissingAttachment); ok {
// Attachment is missing. Just reference it, and skip copying/embedding.
cfg.counts.attachmentsMissing++
log.Printf("WARN: chat file %q - message %d - %s attachment %q (ID %d) - %s", outFile.Name(), msgID, mimeType, transferName, att.ID, err)
if err := outFile.ReferenceAttachment(transferName); err != nil {
return errors.Wrapf(err, "reference attachment %q", transferName)
log.Printf("WARN: chat file %q - message %d - %s attachment %q (ID %d) - %s", outFile.Name(), msgID, att.MIMEType, att.TransferName, att.ID, err)
if err := outFile.ReferenceAttachment(att.TransferName); err != nil {
return errors.Wrapf(err, "reference attachment %q", att.TransferName)
}
cfg.counts.attachments[mimeType]++
cfg.counts.attachments[att.MIMEType]++
continue
} else if err != nil {
return err
Expand All @@ -175,13 +175,12 @@ type errorMissingAttachment struct{ err error }

func (e errorMissingAttachment) Error() string { return e.err.Error() }

func (cfg configuration) validateAttachmentPath(attPath string) error {
if attPath == "" {
func (cfg configuration) validateAttachmentPath(att chatdb.Attachment) error {
if att.Filename == "" {
return errorMissingAttachment{err: errors.New("attachment has no local filename")}
}
attPath = filepath.Join(cfg.Options.AttachmentsPath, attPath)
if ok, err := cfg.OS.FileExist(attPath); err != nil {
return errors.Wrapf(err, "check existence of file %q - POSSIBLE FIX: %s", attPath, _readmeURL)
if ok, err := cfg.OS.FileExist(att.Filepath); err != nil {
return errors.Wrapf(err, "check existence of file %q - POSSIBLE FIX: %s", att.Filepath, _readmeURL)
} else if !ok {
return errorMissingAttachment{err: errors.New("attachment does not exist locally")}
}
Expand All @@ -192,27 +191,25 @@ func (cfg *configuration) copyAttachment(att *chatdb.Attachment, attDir string)
if !cfg.Options.CopyAttachments {
return nil
}
attPath, mimeType := att.Filename, att.MIMEType
unique := true
if cfg.Options.PreservePaths {
unique = false
attDir = filepath.Join(cfg.Options.ExportPath, PreservedPathDir, filepath.Dir(attPath))
attDir = filepath.Join(cfg.Options.ExportPath, PreservedPathDir, filepath.Dir(att.Filename))
if err := cfg.OS.MkdirAll(attDir, os.ModePerm); err != nil {
return errors.Wrapf(err, "create directory %q", attDir)
}
}
attPath = filepath.Join(cfg.Options.AttachmentsPath, attPath)
dstPath, err := cfg.OS.CopyFile(attPath, attDir, unique)
dstPath, err := cfg.OS.CopyFile(att.Filepath, attDir, unique)
if err != nil {
return errors.Wrapf(err, "copy attachment %q to %q", attPath, attDir)
return errors.Wrapf(err, "copy attachment %q to %q", att.Filepath, attDir)
}
att.Filename = dstPath
cfg.counts.attachmentsCopied[mimeType]++
att.Filepath = dstPath
cfg.counts.attachmentsCopied[att.MIMEType]++
return nil
}

func (cfg *configuration) writeAttachment(outFile opsys.OutFile, att chatdb.Attachment) error {
attPath, mimeType := filepath.Join(cfg.Options.AttachmentsPath, att.Filename), att.MIMEType
attPath, mimeType := att.Filepath, att.MIMEType
if cfg.Options.OutputPDF {
if jpgPath, err := cfg.OS.HEIC2JPG(attPath); err != nil {
cfg.counts.conversionsFailed++
Expand Down
Loading

0 comments on commit 9b074a5

Please sign in to comment.