1275 lines
38 KiB
Go
1275 lines
38 KiB
Go
package ofx
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/xml"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/net/html/charset"
|
|
"golang.org/x/text/encoding"
|
|
"golang.org/x/text/encoding/charmap"
|
|
"golang.org/x/text/encoding/unicode"
|
|
"golang.org/x/text/transform"
|
|
|
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
|
)
|
|
|
|
const ofxUnicodeEncoding = "unicode"
|
|
const ofxUSAsciiEncoding = "usascii"
|
|
const ofx1SGMLDataFormat = "OFXSGML"
|
|
|
|
const ofxDataElementName = "OFX"
|
|
|
|
const ofxBankMessageResponseV1ElementName = "BANKMSGSRSV1"
|
|
const ofxCreditCardMessageResponseV1ElementName = "CREDITCARDMSGSRSV1"
|
|
|
|
const ofxBankStatementTransactionResponseElementName = "STMTTRNRS"
|
|
const ofxCreditCardStatementTransactionResponseElementName = "CCSTMTTRNRS"
|
|
|
|
const ofxBankStatementResponseElementName = "STMTRS"
|
|
const ofxCreditCardStatementResponseElementName = "CCSTMTRS"
|
|
|
|
const ofxBankStatementResponseDefaultCurrencyName = "CURDEF"
|
|
const ofxBankStatementResponseBankAccountFromName = "BANKACCTFROM"
|
|
const ofxBankStatementResponseBankTransactionListName = "BANKTRANLIST"
|
|
|
|
const ofxCreditCardStatementResponseDefaultCurrencyName = "CURDEF"
|
|
const ofxCreditCardStatementResponseCreditCardAccountFromName = "CCACCTFROM"
|
|
const ofxCreditCardStatementResponseCreditCardTransactionListName = "BANKTRANLIST"
|
|
|
|
const ofxBankTransactionListStartDateName = "DTSTART"
|
|
const ofxBankTransactionListEndDateName = "DTEND"
|
|
const ofxBankTransactionListStatementTransactionsName = "STMTTRN"
|
|
|
|
const ofxCreditCardTransactionListStartDateName = "DTSTART"
|
|
const ofxCreditCardTransactionListEndDateName = "DTEND"
|
|
const ofxCreditCardTransactionListStatementTransactionsName = "STMTTRN"
|
|
|
|
const ofxBankAccountBankIdName = "BANKID"
|
|
const ofxBankAccountBranchIdName = "BRANCHID"
|
|
const ofxBankAccountAccountIdName = "ACCTID"
|
|
const ofxBankAccountAccountTypeName = "ACCTTYPE"
|
|
const ofxBankAccountAccountKeyName = "ACCTKEY"
|
|
|
|
const ofxCreditCardAccountAccountIdName = "ACCTID"
|
|
const ofxCreditCardAccountAccountKeyName = "ACCTKEY"
|
|
|
|
const ofxTransactionTransactionIdName = "FITID"
|
|
const ofxTransactionTransactionTypeName = "TRNTYPE"
|
|
const ofxTransactionPostedDateName = "DTPOSTED"
|
|
const ofxTransactionAmountName = "TRNAMT"
|
|
const ofxTransactionNameName = "NAME"
|
|
const ofxTransactionMemoName = "MEMO"
|
|
const ofxTransactionCurrencyName = "CURRENCY"
|
|
const ofxTransactionOriginalCurrencyName = "ORIGCURRENCY"
|
|
const ofxTransactionPayeeName = "PAYEE"
|
|
const ofxTransactionBankAccountToName = "BANKACCTTO"
|
|
const ofxTransactionCreditCardAccountToName = "CCACCTTO"
|
|
|
|
const ofxPayeeNameName = "NAME"
|
|
const ofxPayeeAddress1Name = "ADDR1"
|
|
const ofxPayeeAddress2Name = "ADDR2"
|
|
const ofxPayeeAddress3Name = "ADDR3"
|
|
const ofxPayeeCityName = "CITY"
|
|
const ofxPayeeStateName = "STATE"
|
|
const ofxPayeePostalCodeName = "POSTALCODE"
|
|
const ofxPayeeCountryName = "COUNTRY"
|
|
const ofxPayeePhoneName = "PHONE"
|
|
|
|
var ofxBankStatementResponseChildrenNames = map[string]bool{
|
|
ofxBankStatementResponseDefaultCurrencyName: true,
|
|
}
|
|
|
|
var ofxCreditCardStatementResponseChildrenNames = map[string]bool{
|
|
ofxCreditCardStatementResponseDefaultCurrencyName: true,
|
|
}
|
|
|
|
var ofxBankTransactionListChildrenNames = map[string]bool{
|
|
ofxBankTransactionListStartDateName: true,
|
|
ofxBankTransactionListEndDateName: true,
|
|
}
|
|
|
|
var ofxCreditCardTransactionListChildrenNames = map[string]bool{
|
|
ofxCreditCardTransactionListStartDateName: true,
|
|
ofxCreditCardTransactionListEndDateName: true,
|
|
}
|
|
|
|
var ofxBankAccountChildrenNames = map[string]bool{
|
|
ofxBankAccountBankIdName: true,
|
|
ofxBankAccountBranchIdName: true,
|
|
ofxBankAccountAccountIdName: true,
|
|
ofxBankAccountAccountTypeName: true,
|
|
ofxBankAccountAccountKeyName: true,
|
|
}
|
|
|
|
var ofxCreditCardAccountChildrenNames = map[string]bool{
|
|
ofxCreditCardAccountAccountIdName: true,
|
|
ofxCreditCardAccountAccountKeyName: true,
|
|
}
|
|
|
|
var ofxTransactionChildrenNames = map[string]bool{
|
|
ofxTransactionTransactionIdName: true,
|
|
ofxTransactionTransactionTypeName: true,
|
|
ofxTransactionPostedDateName: true,
|
|
ofxTransactionAmountName: true,
|
|
ofxTransactionNameName: true,
|
|
ofxTransactionMemoName: true,
|
|
ofxTransactionCurrencyName: true,
|
|
ofxTransactionOriginalCurrencyName: true,
|
|
}
|
|
|
|
var ofxPayeeChildrenNames = map[string]bool{
|
|
ofxPayeeNameName: true,
|
|
ofxPayeeAddress1Name: true,
|
|
ofxPayeeAddress2Name: true,
|
|
ofxPayeeAddress3Name: true,
|
|
ofxPayeeCityName: true,
|
|
ofxPayeeStateName: true,
|
|
ofxPayeePostalCodeName: true,
|
|
ofxPayeeCountryName: true,
|
|
ofxPayeePhoneName: true,
|
|
}
|
|
|
|
var ofx2HeaderPattern = regexp.MustCompile("<\\?OFX( +[A-Z]+=\"[^=]*\")* *\\?>")
|
|
var ofx2HeaderAttributePattern = regexp.MustCompile(" +([A-Z]+)=\"([^=]*)\"")
|
|
|
|
// ofxFileReader defines the structure of open financial exchange (ofx) file reader
|
|
type ofxFileReader struct {
|
|
fileHeader *ofxFileHeader
|
|
xmlDecoder *xml.Decoder
|
|
}
|
|
|
|
// read returns the imported open financial exchange (ofx) file
|
|
func (r *ofxFileReader) read(ctx core.Context) (*ofxFile, error) {
|
|
var file *ofxFile
|
|
strictMode := true
|
|
|
|
if r.fileHeader != nil && r.fileHeader.OFXDeclarationVersion == ofxVersion1 {
|
|
strictMode = false
|
|
}
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if token.Name.Local == ofxDataElementName {
|
|
file, err = r.readOFXElement(ctx, strictMode, ofxDataElementName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if file == nil {
|
|
log.Errorf(ctx, "[ofxFileReader.read] cannot parse ofx file")
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
file.FileHeader = r.fileHeader
|
|
|
|
return file, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readOFXElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxFile, error) {
|
|
file := &ofxFile{}
|
|
hasEndElement := false
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if token.Name.Local == ofxBankMessageResponseV1ElementName {
|
|
element, err := r.readBankMessageResponseV1Element(ctx, strictMode, ofxBankMessageResponseV1ElementName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
file.BankMessageResponseV1 = element
|
|
} else if token.Name.Local == ofxCreditCardMessageResponseV1ElementName {
|
|
element, err := r.readCreditCardMessageResponseV1Element(ctx, strictMode, ofxCreditCardMessageResponseV1ElementName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
file.CreditCardMessageResponseV1 = element
|
|
}
|
|
case xml.EndElement:
|
|
if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readOFXElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
return file, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readBankMessageResponseV1Element(ctx core.Context, strictMode bool, parentElementName string) (*ofxBankMessageResponseV1, error) {
|
|
response := &ofxBankMessageResponseV1{}
|
|
hasEndElement := false
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if token.Name.Local == ofxBankStatementTransactionResponseElementName {
|
|
element, err := r.readBankStatementTransactionResponseElement(ctx, strictMode, ofxBankStatementTransactionResponseElementName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.StatementTransactionResponse = element
|
|
}
|
|
case xml.EndElement:
|
|
if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankMessageResponseV1Element] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readCreditCardMessageResponseV1Element(ctx core.Context, strictMode bool, parentElementName string) (*ofxCreditCardMessageResponseV1, error) {
|
|
response := &ofxCreditCardMessageResponseV1{}
|
|
hasEndElement := false
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if token.Name.Local == ofxCreditCardStatementTransactionResponseElementName {
|
|
element, err := r.readCreditCardStatementTransactionResponseElement(ctx, strictMode, ofxCreditCardStatementTransactionResponseElementName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.StatementTransactionResponse = element
|
|
}
|
|
case xml.EndElement:
|
|
if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditCardMessageResponseV1Element] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readBankStatementTransactionResponseElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxBankStatementTransactionResponse, error) {
|
|
response := &ofxBankStatementTransactionResponse{}
|
|
hasEndElement := false
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if token.Name.Local == ofxBankStatementResponseElementName {
|
|
element, err := r.readBankStatementResponseElement(ctx, strictMode, ofxBankStatementResponseElementName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.StatementResponse = element
|
|
}
|
|
case xml.EndElement:
|
|
if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankStatementTransactionResponseElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readCreditCardStatementTransactionResponseElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxCreditCardStatementTransactionResponse, error) {
|
|
response := &ofxCreditCardStatementTransactionResponse{}
|
|
hasEndElement := false
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if token.Name.Local == ofxCreditCardStatementResponseElementName {
|
|
element, err := r.readCreditCardStatementResponseElement(ctx, strictMode, ofxCreditCardStatementResponseElementName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.StatementResponse = element
|
|
}
|
|
case xml.EndElement:
|
|
if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditCardStatementTransactionResponseElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readBankStatementResponseElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxBankStatementResponse, error) {
|
|
response := &ofxBankStatementResponse{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxBankStatementResponseChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
} else if token.Name.Local == ofxBankStatementResponseBankAccountFromName {
|
|
element, err := r.readBankAccountElement(ctx, strictMode, ofxBankStatementResponseBankAccountFromName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.AccountFrom = element
|
|
} else if token.Name.Local == ofxBankStatementResponseBankTransactionListName {
|
|
element, err := r.readBankTransactionListElement(ctx, strictMode, ofxBankStatementResponseBankTransactionListName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.TransactionList = element
|
|
}
|
|
case xml.EndElement:
|
|
if ofxBankStatementResponseChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxBankStatementResponseChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankStatementResponseElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankStatementResponseElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxBankStatementResponseDefaultCurrencyName {
|
|
response.DefaultCurrency = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readCreditCardStatementResponseElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxCreditCardStatementResponse, error) {
|
|
response := &ofxCreditCardStatementResponse{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxCreditCardStatementResponseChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
} else if token.Name.Local == ofxCreditCardStatementResponseCreditCardAccountFromName {
|
|
element, err := r.readCreditAccountElement(ctx, strictMode, ofxCreditCardStatementResponseCreditCardAccountFromName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.AccountFrom = element
|
|
} else if token.Name.Local == ofxCreditCardStatementResponseCreditCardTransactionListName {
|
|
element, err := r.readCreditCardTransactionListElement(ctx, strictMode, ofxCreditCardStatementResponseCreditCardTransactionListName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.TransactionList = element
|
|
}
|
|
case xml.EndElement:
|
|
if ofxCreditCardStatementResponseChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxCreditCardStatementResponseChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditCardStatementResponseElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditCardStatementResponseElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxCreditCardStatementResponseDefaultCurrencyName {
|
|
response.DefaultCurrency = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readBankAccountElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxBankAccount, error) {
|
|
account := &ofxBankAccount{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxBankAccountChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
}
|
|
case xml.EndElement:
|
|
if ofxBankAccountChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxBankAccountChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankAccountElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankAccountElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxBankAccountBankIdName {
|
|
account.BankId = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxBankAccountBranchIdName {
|
|
account.BranchId = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxBankAccountAccountIdName {
|
|
account.AccountId = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxBankAccountAccountTypeName {
|
|
account.AccountType = ofxAccountType(r.getActualElementValue(name, value, elementNotHasEndElement, strictMode))
|
|
} else if name == ofxBankAccountAccountKeyName {
|
|
account.AccountKey = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readCreditAccountElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxCreditCardAccount, error) {
|
|
account := &ofxCreditCardAccount{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxCreditCardAccountChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
}
|
|
case xml.EndElement:
|
|
if ofxCreditCardAccountChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxCreditCardAccountChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditAccountElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditAccountElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxCreditCardAccountAccountIdName {
|
|
account.AccountId = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxCreditCardAccountAccountKeyName {
|
|
account.AccountKey = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readBankTransactionListElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxBankTransactionList, error) {
|
|
transactionList := &ofxBankTransactionList{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxBankTransactionListChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
} else if token.Name.Local == ofxBankTransactionListStatementTransactionsName {
|
|
ofxBaseStatementTransaction, backAccountTo, _, err := r.readStatementTransactionElement(ctx, strictMode, "STMTTRN")
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
transaction := &ofxBankStatementTransaction{
|
|
ofxBaseStatementTransaction: *ofxBaseStatementTransaction,
|
|
AccountTo: backAccountTo,
|
|
}
|
|
|
|
transactionList.StatementTransactions = append(transactionList.StatementTransactions, transaction)
|
|
}
|
|
case xml.EndElement:
|
|
if ofxBankTransactionListChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxBankTransactionListChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankTransactionListElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readBankTransactionListElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxBankTransactionListStartDateName {
|
|
transactionList.StartDate = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxBankTransactionListEndDateName {
|
|
transactionList.EndDate = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return transactionList, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readCreditCardTransactionListElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxCreditCardTransactionList, error) {
|
|
transactionList := &ofxCreditCardTransactionList{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxCreditCardTransactionListChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
} else if token.Name.Local == ofxCreditCardTransactionListStatementTransactionsName {
|
|
ofxBaseStatementTransaction, _, creditCardAccountTo, err := r.readStatementTransactionElement(ctx, strictMode, "STMTTRN")
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
transaction := &ofxCreditCardStatementTransaction{
|
|
ofxBaseStatementTransaction: *ofxBaseStatementTransaction,
|
|
AccountTo: creditCardAccountTo,
|
|
}
|
|
|
|
transactionList.StatementTransactions = append(transactionList.StatementTransactions, transaction)
|
|
}
|
|
case xml.EndElement:
|
|
if ofxCreditCardTransactionListChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxCreditCardTransactionListChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditCardTransactionListElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readCreditCardTransactionListElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxCreditCardTransactionListStartDateName {
|
|
transactionList.StartDate = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxCreditCardTransactionListEndDateName {
|
|
transactionList.EndDate = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return transactionList, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readStatementTransactionElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxBaseStatementTransaction, *ofxBankAccount, *ofxCreditCardAccount, error) {
|
|
var bankAccountTo *ofxBankAccount
|
|
var creditCardAccountTo *ofxCreditCardAccount
|
|
transaction := &ofxBaseStatementTransaction{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxTransactionChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
} else if token.Name.Local == ofxTransactionPayeeName {
|
|
element, err := r.readPayeeElement(ctx, strictMode, ofxTransactionPayeeName)
|
|
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
transaction.Payee = element
|
|
} else if token.Name.Local == ofxTransactionBankAccountToName {
|
|
element, err := r.readBankAccountElement(ctx, strictMode, ofxTransactionBankAccountToName)
|
|
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
bankAccountTo = element
|
|
} else if token.Name.Local == ofxTransactionCreditCardAccountToName {
|
|
element, err := r.readCreditAccountElement(ctx, strictMode, ofxTransactionCreditCardAccountToName)
|
|
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
creditCardAccountTo = element
|
|
}
|
|
case xml.EndElement:
|
|
if ofxTransactionChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxTransactionChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readStatementTransactionElement] not found </%s> element", parentElementName)
|
|
return nil, nil, nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readStatementTransactionElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, nil, nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxTransactionTransactionIdName {
|
|
transaction.TransactionId = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxTransactionTransactionTypeName {
|
|
transaction.TransactionType = ofxTransactionType(r.getActualElementValue(name, value, elementNotHasEndElement, strictMode))
|
|
} else if name == ofxTransactionPostedDateName {
|
|
transaction.PostedDate = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxTransactionAmountName {
|
|
transaction.Amount = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxTransactionNameName {
|
|
transaction.Name = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxTransactionMemoName {
|
|
transaction.Memo = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxTransactionCurrencyName {
|
|
transaction.Currency = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxTransactionOriginalCurrencyName {
|
|
transaction.OriginalCurrency = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return transaction, bankAccountTo, creditCardAccountTo, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) readPayeeElement(ctx core.Context, strictMode bool, parentElementName string) (*ofxPayee, error) {
|
|
payee := &ofxPayee{}
|
|
hasEndElement := false
|
|
elementNotHasEndElement := make(map[string]bool)
|
|
elementValues := make(map[string]string)
|
|
currentElementName := ""
|
|
|
|
for {
|
|
token, err := r.xmlDecoder.RawToken()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
switch token := token.(type) {
|
|
case xml.StartElement:
|
|
if ofxPayeeChildrenNames[token.Name.Local] {
|
|
currentElementName = token.Name.Local
|
|
elementNotHasEndElement[token.Name.Local] = true
|
|
}
|
|
case xml.EndElement:
|
|
if ofxPayeeChildrenNames[token.Name.Local] {
|
|
delete(elementNotHasEndElement, token.Name.Local)
|
|
} else if token.Name.Local == parentElementName {
|
|
hasEndElement = true
|
|
break
|
|
}
|
|
case xml.CharData:
|
|
if ofxPayeeChildrenNames[currentElementName] {
|
|
elementValues[currentElementName] = string(token)
|
|
}
|
|
|
|
currentElementName = ""
|
|
}
|
|
|
|
if hasEndElement {
|
|
break
|
|
}
|
|
}
|
|
|
|
if strictMode && !hasEndElement {
|
|
log.Errorf(ctx, "[ofxFileReader.readPayeeElement] not found </%s> element", parentElementName)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if strictMode && len(elementNotHasEndElement) > 0 {
|
|
log.Errorf(ctx, "[ofxFileReader.readPayeeElement] not found end element for %s", r.getNotHasEndElementNames(elementNotHasEndElement))
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
for name, value := range elementValues {
|
|
if name == ofxPayeeNameName {
|
|
payee.Name = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeeAddress1Name {
|
|
payee.Address1 = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeeAddress2Name {
|
|
payee.Address2 = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeeAddress3Name {
|
|
payee.Address3 = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeeCityName {
|
|
payee.City = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeeStateName {
|
|
payee.State = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeePostalCodeName {
|
|
payee.PostalCode = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeeCountryName {
|
|
payee.Country = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
} else if name == ofxPayeePhoneName {
|
|
payee.Phone = r.getActualElementValue(name, value, elementNotHasEndElement, strictMode)
|
|
}
|
|
}
|
|
|
|
return payee, nil
|
|
}
|
|
|
|
func (r *ofxFileReader) getActualElementValue(name string, value string, elementNotHasEndElement map[string]bool, strictMode bool) string {
|
|
if strictMode {
|
|
return value
|
|
}
|
|
|
|
_, notHasEndElement := elementNotHasEndElement[name]
|
|
|
|
if !notHasEndElement {
|
|
return value
|
|
}
|
|
|
|
for i := 0; i < len(value); i++ {
|
|
if value[i] == '\r' || value[i] == '\n' {
|
|
return value[0:i]
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func (r *ofxFileReader) getNotHasEndElementNames(elementNotHasEndElement map[string]bool) string {
|
|
builder := strings.Builder{}
|
|
|
|
for name := range elementNotHasEndElement {
|
|
if builder.Len() > 0 {
|
|
builder.WriteRune(',')
|
|
}
|
|
|
|
builder.WriteString(name)
|
|
}
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
func createNewOFXFileReader(ctx core.Context, data []byte) (*ofxFileReader, error) {
|
|
firstNonCrLfIndex := 0
|
|
|
|
for i := 0; i < len(data); i++ {
|
|
if data[i] != '\n' && data[i] != '\r' {
|
|
firstNonCrLfIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(data) > 5 && string(data[firstNonCrLfIndex:firstNonCrLfIndex+5]) == "<?xml" { // ofx 2.x starts with <?xml
|
|
return createNewOFX2FileReader(ctx, data, true)
|
|
} else if len(data) > 10 && string(data[firstNonCrLfIndex:firstNonCrLfIndex+10]) == "OFXHEADER:" { // ofx 1.x starts with OFXHEADER:
|
|
return createNewOFX1FileReader(ctx, data)
|
|
} else if len(data) > 5 && string(data[firstNonCrLfIndex:firstNonCrLfIndex+5]) == "<OFX>" { // no ofx header
|
|
return createNewOFX2FileReader(ctx, data, false)
|
|
}
|
|
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
func createNewOFX1FileReader(ctx core.Context, data []byte) (*ofxFileReader, error) {
|
|
fileHeader, fileData, dataType, enc, err := readOFX1FileHeader(ctx, data)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fileHeader.OFXDeclarationVersion != ofxVersion1 {
|
|
log.Errorf(ctx, "[ofx_data_reader.createNewOFX1FileReader] cannot parse ofx 1.x file header, because declaration version is \"%s\"", fileHeader.OFXDeclarationVersion)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
if dataType != ofx1SGMLDataFormat {
|
|
log.Errorf(ctx, "[ofx_data_reader.createNewOFX1FileReader] cannot parse ofx 1.x file header, because data type is \"%s\"", dataType)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
reader := bytes.NewReader(fileData)
|
|
buffer := &bytes.Buffer{}
|
|
|
|
if enc != nil {
|
|
transformReader := transform.NewReader(reader, enc.NewDecoder())
|
|
_, err = buffer.ReadFrom(transformReader)
|
|
} else {
|
|
_, err = buffer.ReadFrom(reader)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Errorf(ctx, "[ofx_data_reader.createNewOFX1FileReader] cannot read ofx 1.x file content, because %s", err.Error())
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
sgmlData := buffer.String()
|
|
stringReader := strings.NewReader(sgmlData)
|
|
|
|
xmlDecoder := xml.NewDecoder(stringReader)
|
|
xmlDecoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
|
|
return input, nil
|
|
}
|
|
|
|
return &ofxFileReader{
|
|
fileHeader: fileHeader,
|
|
xmlDecoder: xmlDecoder,
|
|
}, nil
|
|
}
|
|
|
|
func createNewOFX2FileReader(ctx core.Context, data []byte, withHeader bool) (*ofxFileReader, error) {
|
|
var fileHeader *ofxFileHeader = nil
|
|
var err error
|
|
|
|
if withHeader {
|
|
fileHeader, err = readOFX2FileHeader(ctx, data)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fileHeader.OFXDeclarationVersion != ofxVersion2 {
|
|
log.Errorf(ctx, "[ofx_data_reader.createNewOFX2FileReader] cannot parse ofx 2.x file header, because declaration version is \"%s\"", fileHeader.OFXDeclarationVersion)
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
}
|
|
|
|
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
|
|
xmlDecoder.CharsetReader = charset.NewReaderLabel
|
|
|
|
return &ofxFileReader{
|
|
fileHeader: fileHeader,
|
|
xmlDecoder: xmlDecoder,
|
|
}, nil
|
|
}
|
|
|
|
func readOFX1FileHeader(ctx core.Context, data []byte) (fileHeader *ofxFileHeader, fileData []byte, dataType string, enc encoding.Encoding, err error) {
|
|
fileHeader = &ofxFileHeader{}
|
|
dataType = ""
|
|
fileEncoding := ""
|
|
fileCharset := ""
|
|
fileDataStartPosition := 0
|
|
lastCrLf := -1
|
|
|
|
for i := 0; i < len(data); i++ {
|
|
if data[i] != '\n' && data[i] != '\r' {
|
|
continue
|
|
}
|
|
|
|
if lastCrLf == i-1 {
|
|
lastCrLf = i
|
|
continue
|
|
}
|
|
|
|
line := string(data[lastCrLf+1 : i])
|
|
|
|
if strings.Index(line, "<OFX>") == 0 {
|
|
fileDataStartPosition = lastCrLf + 1
|
|
break
|
|
}
|
|
|
|
lastCrLf = i
|
|
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
items := strings.Split(line, ":")
|
|
|
|
if len(items) != 2 {
|
|
log.Warnf(ctx, "[ofx_data_reader.readOFX1FileHeader] cannot parse line in ofx 1.x file header, because line is \"%s\"", line)
|
|
continue
|
|
}
|
|
|
|
key := items[0]
|
|
value := items[1]
|
|
|
|
if key == "OFXHEADER" {
|
|
fileHeader.OFXDeclarationVersion = oFXDeclarationVersion(value)
|
|
} else if key == "DATA" {
|
|
dataType = value
|
|
} else if key == "VERSION" {
|
|
fileHeader.OFXDataVersion = value
|
|
} else if key == "SECURITY" {
|
|
fileHeader.Security = value
|
|
} else if key == "ENCODING" {
|
|
fileEncoding = strings.ToLower(value)
|
|
} else if key == "CHARSET" {
|
|
fileCharset = strings.ToLower(value)
|
|
} else if key == "COMPRESSION" {
|
|
continue // ignore
|
|
} else if key == "OLDFILEUID" {
|
|
fileHeader.OldFileUid = value
|
|
} else if key == "NEWFILEUID" {
|
|
fileHeader.NewFileUid = value
|
|
} else {
|
|
log.Warnf(ctx, "[ofx_data_reader.readOFX1FileHeader] cannot parse unknown header line in ofx 1.x file header, because line is \"%s\"", line)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if fileEncoding == ofxUSAsciiEncoding {
|
|
if utils.IsStringOnlyContainsDigits(fileCharset) {
|
|
fileCharset = "cp" + fileCharset
|
|
}
|
|
|
|
enc, _ = charset.Lookup(fileCharset)
|
|
|
|
if enc == nil {
|
|
enc, _ = charset.Lookup("us-ascii")
|
|
}
|
|
|
|
if enc == nil {
|
|
enc = charmap.Windows1252
|
|
}
|
|
} else if fileEncoding == ofxUnicodeEncoding {
|
|
enc, _ = charset.Lookup(ofxUnicodeEncoding)
|
|
|
|
if enc == nil {
|
|
enc = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
|
|
}
|
|
} else {
|
|
log.Errorf(ctx, "[ofx_data_reader.readOFX1FileHeader] cannot parse ofx 1.x file, because encoding \"%s\" is unknown", fileEncoding)
|
|
return nil, nil, "", nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
return fileHeader, data[fileDataStartPosition:], dataType, enc, nil
|
|
}
|
|
|
|
func readOFX2FileHeader(ctx core.Context, data []byte) (fileHeader *ofxFileHeader, err error) {
|
|
reader := bytes.NewReader(data)
|
|
scanner := bufio.NewScanner(reader)
|
|
fileHeader = &ofxFileHeader{}
|
|
headerLine := ""
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
ofxHeaderStartIndex := strings.Index(line, "<?OFX ")
|
|
|
|
if ofxHeaderStartIndex >= 0 {
|
|
headerLine = ofx2HeaderPattern.FindString(line)
|
|
break
|
|
}
|
|
}
|
|
|
|
if headerLine == "" {
|
|
log.Errorf(ctx, "[ofx_data_reader.readOFX2FileHeader] cannot find ofx 2.x file header")
|
|
return nil, errs.ErrInvalidOFXFile
|
|
}
|
|
|
|
headerAttributes := ofx2HeaderAttributePattern.FindAllStringSubmatch(headerLine, -1)
|
|
|
|
for _, attributeItems := range headerAttributes {
|
|
if len(attributeItems) != 3 {
|
|
log.Warnf(ctx, "[ofx_data_reader.readOFX2FileHeader] cannot parse line in ofx 2.x file header, because item is \"%s\"", attributeItems)
|
|
continue
|
|
}
|
|
|
|
name := attributeItems[1]
|
|
value := attributeItems[2]
|
|
|
|
if name == "OFXHEADER" {
|
|
fileHeader.OFXDeclarationVersion = oFXDeclarationVersion(value)
|
|
} else if name == "VERSION" {
|
|
fileHeader.OFXDataVersion = value
|
|
} else if name == "SECURITY" {
|
|
fileHeader.Security = value
|
|
} else if name == "OLDFILEUID" {
|
|
fileHeader.OldFileUid = value
|
|
} else if name == "NEWFILEUID" {
|
|
fileHeader.NewFileUid = value
|
|
} else {
|
|
log.Warnf(ctx, "[ofx_data_reader.readOFX2FileHeader] cannot parse unknown header line in ofx 2.x file header, because item is \"%s\"", attributeItems)
|
|
continue
|
|
}
|
|
}
|
|
|
|
return fileHeader, nil
|
|
}
|