Repository: qax-os/excelize

Issue:
support setting excel custom properties (properties in DocProps/custom.xml)
### Description

this feature can support following use case: 
1. support save some biz k:v data to excel custom properties to do such that save excel biz version etc.

### Additional context

I am implementing it.

### Validations

- [x] Check that there isn't already an issue that requests the same feature to avoid creating a duplicate.

Discussion:

Solution patch:
diff --git a/docProps.go b/docProps.go
index 975f51b..49cbdaa 100644
--- a/docProps.go
+++ b/docProps.go
@@ -16,6 +16,8 @@ import (
 	"encoding/xml"
 	"io"
 	"reflect"
+	"slices"
+	"time"
 )
 
 // SetAppProps provides a function to set document application properties. The
@@ -236,3 +238,117 @@ func (f *File) GetDocProps() (ret *DocProperties, err error) {
 	}
 	return
 }
+
+// SetCustomProps provides a function to set custom file properties by given
+// property name and value. If the property name already exists, it will be
+// updated, otherwise a new property will be added. The value can be of type
+// int32, float64, bool, string, time.Time or nil. The property will be delete
+// if the value is nil. The function returns an error if the property value is
+// not of the correct type.
+func (f *File) SetCustomProps(prop CustomProperty) error {
+	if prop.Name == "" {
+		return ErrParameterInvalid
+	}
+	props := new(decodeCustomProperties)
+	if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
+		Decode(props); err != nil && err != io.EOF {
+		return err
+	}
+	customProps := xlsxCustomProperties{Vt: NameSpaceDocumentPropertiesVariantTypes.Value}
+	idx, pID := -1, 1
+	for i := range props.Property {
+		p := new(xlsxProperty)
+		setPtrFields(reflect.ValueOf(&props.Property[i]).Elem(), reflect.ValueOf(p).Elem())
+		if pID < props.Property[i].PID {
+			pID = props.Property[i].PID
+		}
+		if props.Property[i].Name == prop.Name {
+			idx = i
+		}
+		customProps.Property = append(customProps.Property, *p)
+	}
+	if idx != -1 && prop.Value == nil {
+		customProps.Property = slices.Delete(customProps.Property, idx, idx+1)
+	}
+	if prop.Value != nil {
+		property := xlsxProperty{Name: prop.Name, FmtID: EXtURICustomPropertyFmtID}
+		if err := property.setCustomProps(prop.Value); err != nil {
+			return err
+		}
+		if idx != -1 {
+			property.PID = customProps.Property[idx].PID
+			customProps.Property[idx] = property
+		} else {
+			property.PID = pID + 1
+			customProps.Property = append(customProps.Property, property)
+		}
+	}
+	_ = f.addRels(defaultXMLPathRels, SourceRelationshipCustomProperties, defaultXMLPathDocPropsCustom, "")
+	if err := f.addContentTypePart(0, "customProperties"); err != nil {
+		return err
+	}
+	output, err := xml.Marshal(customProps)
+	f.saveFileList(defaultXMLPathDocPropsCustom, output)
+	return err
+}
+
+// setCustomProps sets the custom property value based on its type.
+func (prop *xlsxProperty) setCustomProps(value interface{}) error {
+	switch v := value.(type) {
+	case int32:
+		prop.I4 = &v
+	case float64:
+		prop.R8 = float64Ptr(v)
+	case bool:
+		prop.Bool = boolPtr(v)
+	case string:
+		prop.Lpwstr = stringPtr(value.(string))
+	case time.Time:
+		prop.FileTime = stringPtr(value.(time.Time).Format(time.RFC3339))
+	default:
+		return ErrParameterInvalid
+	}
+	return nil
+}
+
+// GetCustomProps provides a function to get custom file properties.
+func (f *File) GetCustomProps() ([]CustomProperty, error) {
+	var customProps []CustomProperty
+	props := new(decodeCustomProperties)
+	if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
+		Decode(props); err != nil && err != io.EOF {
+		return customProps, err
+	}
+	for _, p := range props.Property {
+		prop := CustomProperty{Name: p.Name}
+		var err error
+		if prop.Value, err = p.getCustomProps(); err != nil {
+			return customProps, err
+		}
+		customProps = append(customProps, prop)
+	}
+	return customProps, nil
+}
+
+// getCustomProps gets the custom property value based on its type.
+func (p *decodeProperty) getCustomProps() (interface{}, error) {
+	s := reflect.ValueOf(p).Elem()
+	for i := range s.NumField() {
+		if 11 <= i && i <= 20 && !s.Field(i).IsNil() {
+			return int32(s.Field(i).Elem().Int()), nil // Field vt:i1 to vt:uint
+		}
+		if 21 <= i && i <= 22 && !s.Field(i).IsNil() {
+			return s.Field(i).Elem().Float(), nil // Field vt:r4 to vt:r8
+		}
+		if p.Bool != nil {
+			return *p.Bool, nil
+		}
+		if p.Lpwstr != nil {
+			return *p.Lpwstr, nil
+		}
+		if p.FileTime != nil {
+			return time.Parse(time.RFC3339, *p.FileTime)
+		}
+	}
+	return nil, nil
+}
diff --git a/excelize.go b/excelize.go
index 61bb6d3..52b7fa3 100644
--- a/excelize.go
+++ b/excelize.go
@@ -419,7 +419,8 @@ func (f *File) setRels(rID, relPath, relType, target, targetMode string) int {
 // relationship type, target and target mode.
 func (f *File) addRels(relPath, relType, target, targetMode string) int {
 	uniqPart := map[string]string{
-		SourceRelationshipSharedStrings: "/xl/sharedStrings.xml",
+		SourceRelationshipCustomProperties: "/docProps/custom.xml",
+		SourceRelationshipSharedStrings:    "/xl/sharedStrings.xml",
 	}
 	rels, _ := f.relsReader(relPath)
 	if rels == nil {
diff --git a/file.go b/file.go
index 1ef8a8a..03036c9 100644
--- a/file.go
+++ b/file.go
@@ -31,7 +31,7 @@ import (
 //	f := NewFile()
 func NewFile(opts ...Options) *File {
 	f := newFile()
-	f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
+	f.Pkg.Store(defaultXMLPathRels, []byte(xml.Header+templateRels))
 	f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
 	f.Pkg.Store(defaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore))
 	f.Pkg.Store(defaultXMLPathWorkbookRels, []byte(xml.Header+templateWorkbookRels))
diff --git a/lib.go b/lib.go
index 4cd0552..7eeb813 100644
--- a/lib.go
+++ b/lib.go
@@ -892,6 +892,19 @@ func assignFieldValue(field string, immutable, mutable reflect.Value) {
 	}
 }
 
+// setPtrFields assigns the fields of the immutable struct to the mutable
+// struct. The fields name of the immutable struct must match the field names of
+// the mutable struct.
+func setPtrFields(immutable, mutable reflect.Value) {
+	for i := range immutable.NumField() {
+		srcField := immutable.Type().Field(i)
+		dstField := mutable.FieldByName(srcField.Name)
+		if dstField.IsValid() && dstField.CanSet() && dstField.Type() == immutable.Field(i).Type() {
+			dstField.Set(immutable.Field(i))
+		}
+	}
+}
+
 // setNoPtrFieldsVal assigns values from the pointer or no-pointer structs
 // fields (immutable) value to no-pointer struct field.
 func setNoPtrFieldsVal(fields []string, immutable, mutable reflect.Value) {
diff --git a/templates.go b/templates.go
index b8cf159..bdcb375 100644
--- a/templates.go
+++ b/templates.go
@@ -43,6 +43,7 @@ var (
 // Source relationship and namespace.
 const (
 	ContentTypeAddinMacro                         = "application/vnd.ms-excel.addin.macroEnabled.main+xml"
+	ContentTypeCustomProperties                   = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
 	ContentTypeDrawing                            = "application/vnd.openxmlformats-officedocument.drawing+xml"
 	ContentTypeDrawingML                          = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
 	ContentTypeMacro                              = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
@@ -71,6 +72,7 @@ const (
 	SourceRelationshipChart                       = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
 	SourceRelationshipChartsheet                  = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
 	SourceRelationshipComments                    = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+	SourceRelationshipCustomProperties            = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
 	SourceRelationshipDialogsheet                 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
 	SourceRelationshipDrawingML                   = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
 	SourceRelationshipDrawingVML                  = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
@@ -102,6 +104,7 @@ const (
 	ExtURICalcFeatures                   = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"
 	ExtURIConditionalFormattingRuleID    = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
 	ExtURIConditionalFormattings         = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
+	EXtURICustomPropertyFmtID            = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
 	ExtURIDataField                      = "{E15A36E0-9728-4E99-A89B-3F7291B0FE68}"
 	ExtURIDataModel                      = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}"
 	ExtURIDataValidations                = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"
@@ -278,6 +281,8 @@ const (
 	defaultXMLPathContentTypes            = "[Content_Types].xml"
 	defaultXMLPathDocPropsApp             = "docProps/app.xml"
 	defaultXMLPathDocPropsCore            = "docProps/core.xml"
+	defaultXMLPathDocPropsCustom          = "docProps/custom.xml"
+	defaultXMLPathRels                    = "_rels/.rels"
 	defaultXMLPathSharedStrings           = "xl/sharedStrings.xml"
 	defaultXMLPathStyles                  = "xl/styles.xml"
 	defaultXMLPathTheme                   = "xl/theme/theme1.xml"
diff --git a/workbook.go b/workbook.go
index d1cb1da..d05acba 100644
--- a/workbook.go
+++ b/workbook.go
@@ -198,7 +198,7 @@ func (f *File) setWorkbook(name string, sheetID, rid int) {
 // getWorkbookPath provides a function to get the path of the workbook.xml in
 // the spreadsheet.
 func (f *File) getWorkbookPath() (path string) {
-	if rels, _ := f.relsReader("_rels/.rels"); rels != nil {
+	if rels, _ := f.relsReader(defaultXMLPathRels); rels != nil {
 		rels.mu.Lock()
 		defer rels.mu.Unlock()
 		for _, rel := range rels.Relationships {
@@ -364,28 +364,30 @@ func (f *File) addContentTypePart(index int, contentType string) error {
 		"drawings": f.setContentTypePartImageExtensions,
 	}
 	partNames := map[string]string{
-		"chart":         "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
-		"chartsheet":    "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
-		"comments":      "/xl/comments" + strconv.Itoa(index) + ".xml",
-		"drawings":      "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
-		"table":         "/xl/tables/table" + strconv.Itoa(index) + ".xml",
-		"pivotTable":    "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
-		"pivotCache":    "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
-		"sharedStrings": "/xl/sharedStrings.xml",
-		"slicer":        "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
-		"slicerCache":   "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
+		"chart":            "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
+		"chartsheet":       "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
+		"comments":         "/xl/comments" + strconv.Itoa(index) + ".xml",
+		"customProperties": "/docProps/custom.xml",
+		"drawings":         "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
+		"table":            "/xl/tables/table" + strconv.Itoa(index) + ".xml",
+		"pivotTable":       "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
+		"pivotCache":       "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
+		"sharedStrings":    "/xl/sharedStrings.xml",
+		"slicer":           "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
+		"slicerCache":      "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
 	}
 	contentTypes := map[string]string{
-		"chart":         ContentTypeDrawingML,
-		"chartsheet":    ContentTypeSpreadSheetMLChartsheet,
-		"comments":      ContentTypeSpreadSheetMLComments,
-		"drawings":      ContentTypeDrawing,
-		"table":         ContentTypeSpreadSheetMLTable,
-		"pivotTable":    ContentTypeSpreadSheetMLPivotTable,
-		"pivotCache":    ContentTypeSpreadSheetMLPivotCacheDefinition,
-		"sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
-		"slicer":        ContentTypeSlicer,
-		"slicerCache":   ContentTypeSlicerCache,
+		"chart":            ContentTypeDrawingML,
+		"chartsheet":       ContentTypeSpreadSheetMLChartsheet,
+		"comments":         ContentTypeSpreadSheetMLComments,
+		"customProperties": ContentTypeCustomProperties,
+		"drawings":         ContentTypeDrawing,
+		"table":            ContentTypeSpreadSheetMLTable,
+		"pivotTable":       ContentTypeSpreadSheetMLPivotTable,
+		"pivotCache":       ContentTypeSpreadSheetMLPivotCacheDefinition,
+		"sharedStrings":    ContentTypeSpreadSheetMLSharedStrings,
+		"slicer":           ContentTypeSlicer,
+		"slicerCache":      ContentTypeSlicerCache,
 	}
 	s, ok := setContentType[contentType]
 	if ok {
diff --git a/xmlCustom.go b/xmlCustom.go
new file mode 100644
index 0000000..c0c1c52
--- /dev/null
+++ b/xmlCustom.go
@@ -0,0 +1,127 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import "encoding/xml"
+
+// xlsxCustomProperties directly maps the element for the custom file properties
+// part, that represents additional information. The information can be used as
+// metadata for XML.
+type xlsxCustomProperties struct {
+	XMLName  xml.Name       `xml:"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties Properties"`
+	Vt       string         `xml:"xmlns:vt,attr"`
+	Property []xlsxProperty `xml:"property"`
+}
+
+// xlsxProperty directly maps the element specifies a single custom file
+// property. Custom file property type is defined through child elements in the
+// File Properties Variant Type namespace. Custom file property value can be set
+// by setting the appropriate Variant Type child element value.
+type xlsxProperty struct {
+	XMLName    xml.Name `xml:"property"`
+	FmtID      string   `xml:"fmtid,attr"`
+	PID        int      `xml:"pid,attr"`
+	Name       string   `xml:"name,attr,omitempty"`
+	LinkTarget string   `xml:"linkTarget,attr,omitempty"`
+	Vector     *string  `xml:"vt:vector"`
+	Array      *string  `xml:"vt:array"`
+	Blob       *string  `xml:"vt:blob"`
+	Oblob      *string  `xml:"vt:oblob"`
+	Empty      *string  `xml:"vt:empty"`
+	Null       *string  `xml:"vt:null"`
+	I1         *int8    `xml:"vt:i1"`
+	I2         *int16   `xml:"vt:i2"`
+	I4         *int32   `xml:"vt:i4"`
+	I8         *int64   `xml:"vt:i8"`
+	Int        *int     `xml:"vt:int"`
+	Ui1        *uint8   `xml:"vt:ui1"`
+	Ui2        *uint16  `xml:"vt:ui2"`
+	Ui4        *uint32  `xml:"vt:ui4"`
+	Ui8        *uint64  `xml:"vt:ui8"`
+	Uint       *uint    `xml:"vt:uint"`
+	R4         *float32 `xml:"vt:r4"`
+	R8         *float64 `xml:"vt:r8"`
+	Decimal    *string  `xml:"vt:decimal"`
+	Lpstr      *string  `xml:"vt:lpstr"`
+	Lpwstr     *string  `xml:"vt:lpwstr"`
+	Bstr       *string  `xml:"vt:bstr"`
+	Date       *string  `xml:"vt:date"`
+	FileTime   *string  `xml:"vt:filetime"`
+	Bool       *bool    `xml:"vt:bool"`
+	Cy         *string  `xml:"vt:cy"`
+	Error      *string  `xml:"vt:error"`
+	Stream     *string  `xml:"vt:stream"`
+	Ostream    *string  `xml:"vt:ostream"`
+	Storage    *string  `xml:"vt:storage"`
+	Ostorage   *string  `xml:"vt:ostorage"`
+	Vstream    *string  `xml:"vt:vstream"`
+	ClsID      *string  `xml:"vt:clsid"`
+}
+
+// decodeCustomProperties specifies to an OOXML document custom properties.
+// decodeCustomProperties just for deserialization.
+type decodeCustomProperties struct {
+	XMLName  xml.Name         `xml:"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties Properties"`
+	Vt       string           `xml:"xmlns:vt,attr"`
+	Property []decodeProperty `xml:"property"`
+}
+
+// decodeProperty specifies to an OOXML document custom property. decodeProperty
+// just for deserialization.
+type decodeProperty struct {
+	XMLName    xml.Name `xml:"property"`
+	FmtID      string   `xml:"fmtid,attr"`
+	PID        int      `xml:"pid,attr"`
+	Name       string   `xml:"name,attr,omitempty"`
+	LinkTarget string   `xml:"linkTarget,attr,omitempty"`
+	Vector     *string  `xml:"vector"`
+	Array      *string  `xml:"array"`
+	Blob       *string  `xml:"blob"`
+	Oblob      *string  `xml:"oblob"`
+	Empty      *string  `xml:"empty"`
+	Null       *string  `xml:"null"`
+	I1         *int8    `xml:"i1"`
+	I2         *int16   `xml:"i2"`
+	I4         *int32   `xml:"i4"`
+	I8         *int64   `xml:"i8"`
+	Int        *int     `xml:"int"`
+	Ui1        *uint8   `xml:"ui1"`
+	Ui2        *uint16  `xml:"ui2"`
+	Ui4        *uint32  `xml:"ui4"`
+	Ui8        *uint64  `xml:"ui8"`
+	Uint       *uint    `xml:"uint"`
+	R4         *float32 `xml:"r4"`
+	R8         *float64 `xml:"r8"`
+	Decimal    *string  `xml:"decimal"`
+	Lpstr      *string  `xml:"lpstr"`
+	Lpwstr     *string  `xml:"lpwstr"`
+	Bstr       *string  `xml:"bstr"`
+	Date       *string  `xml:"date"`
+	FileTime   *string  `xml:"filetime"`
+	Bool       *bool    `xml:"bool"`
+	Cy         *string  `xml:"cy"`
+	Error      *string  `xml:"error"`
+	Stream     *string  `xml:"stream"`
+	Ostream    *string  `xml:"ostream"`
+	Storage    *string  `xml:"storage"`
+	Ostorage   *string  `xml:"ostorage"`
+	Vstream    *string  `xml:"vstream"`
+	ClsID      *string  `xml:"clsid"`
+}
+
+// CustomProperty directly maps the custom property of the workbook. The value
+// date type may be one of the following: int32, float64, string, bool,
+// time.Time, or nil.
+type CustomProperty struct {
+	Name  string
+	Value interface{}
+}



Test patch:
diff --git a/docProps_test.go b/docProps_test.go
index dc26e2b..014b377 100644
--- a/docProps_test.go
+++ b/docProps_test.go
@@ -12,8 +12,11 @@
 package excelize
 
 import (
+	"fmt"
 	"path/filepath"
+	"slices"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -116,3 +119,70 @@ func TestGetDocProps(t *testing.T) {
 	_, err = f.GetDocProps()
 	assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
 }
+
+func TestCustomProps(t *testing.T) {
+	f := NewFile()
+	expected := []CustomProperty{
+		{Name: "Text Prop", Value: "text"},
+		{Name: "Boolean Prop 1", Value: true},
+		{Name: "Boolean Prop 2", Value: false},
+		{Name: "Number Prop 1", Value: -123.456},
+		{Name: "Number Prop 2", Value: int32(1)},
+		{Name: "Date Prop", Value: time.Date(2021, time.September, 11, 0, 0, 0, 0, time.UTC)},
+	}
+	for _, prop := range expected {
+		assert.NoError(t, f.SetCustomProps(prop))
+	}
+	props, err := f.GetCustomProps()
+	assert.NoError(t, err)
+	assert.Equal(t, expected, props)
+
+	// Test delete custom property
+	assert.NoError(t, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 1", Value: nil}))
+	props, err = f.GetCustomProps()
+	assert.NoError(t, err)
+	expected = slices.Delete(expected, 1, 2)
+	assert.Equal(t, expected, props)
+
+	// Test change custom property value data type
+	assert.NoError(t, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 2", Value: "true"}))
+	props, err = f.GetCustomProps()
+	assert.NoError(t, err)
+	assert.Equal(t, props[1].Value, "true")
+
+	// Test set custom property with unsupported value data type
+	assert.Equal(t, ErrParameterInvalid, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 2", Value: 1}))
+
+	assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCustomProps.xlsx")))
+	assert.NoError(t, f.Close())
+
+	// Test set custom property without property name
+	f = NewFile()
+	assert.Equal(t, ErrParameterInvalid, f.SetCustomProps(CustomProperty{}))
+
+	// Test set custom property with unsupported charset
+	f.Pkg.Store(defaultXMLPathDocPropsCustom, MacintoshCyrillicCharset)
+	assert.EqualError(t, f.SetCustomProps(CustomProperty{Name: "Prop"}), "XML syntax error on line 1: invalid UTF-8")
+
+	// Test get custom property with unsupported charset
+	_, err = f.GetCustomProps()
+	assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+	// Test set custom property with unsupported charset content types
+	f = NewFile()
+	f.ContentTypes = nil
+	f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+	assert.EqualError(t, f.SetCustomProps(CustomProperty{Name: "Prop"}), "XML syntax error on line 1: invalid UTF-8")
+
+	// Test get custom property with unsupported charset
+	f.Pkg.Store(defaultXMLPathDocPropsCustom, []byte(fmt.Sprintf(`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="%s"><property fmtid="%s" pid="2" name="Prop"><vt:filetime>x</vt:filetime></property></Properties>`, NameSpaceDocumentPropertiesVariantTypes, EXtURICustomPropertyFmtID)))
+	_, err = f.GetCustomProps()
+	assert.EqualError(t, err, "parsing time \"x\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"x\" as \"2006\"")
+
+	// Test get custom property with unsupported value data type
+	f.Pkg.Store(defaultXMLPathDocPropsCustom, []byte(fmt.Sprintf(`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="%s"><property fmtid="%s" pid="2" name="Prop"><vt:cy></vt:cy></property></Properties>`, NameSpaceDocumentPropertiesVariantTypes, EXtURICustomPropertyFmtID)))
+	props, err = f.GetCustomProps()
+	assert.Equal(t, []CustomProperty{{Name: "Prop"}}, props)
+	assert.NoError(t, err)
+	assert.NoError(t, f.Close())
+}
diff --git a/sheet_test.go b/sheet_test.go
index d5cc4cf..dc82346 100644
--- a/sheet_test.go
+++ b/sheet_test.go
@@ -524,14 +524,14 @@ func TestWorksheetWriter(t *testing.T) {
 
 func TestGetWorkbookPath(t *testing.T) {
 	f := NewFile()
-	f.Pkg.Delete("_rels/.rels")
+	f.Pkg.Delete(defaultXMLPathRels)
 	assert.Empty(t, f.getWorkbookPath())
 }
 
 func TestGetWorkbookRelsPath(t *testing.T) {
 	f := NewFile()
 	f.Pkg.Delete("xl/_rels/.rels")
-	f.Pkg.Store("_rels/.rels", []byte(xml.Header+`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" Target="/workbook.xml"/></Relationships>`))
+	f.Pkg.Store(defaultXMLPathRels, []byte(xml.Header+`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" Target="/workbook.xml"/></Relationships>`))
 	assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath())
 }