Skip to content

Commit

Permalink
First pass at solving lestrrat-go#67
Browse files Browse the repository at this point in the history
  • Loading branch information
lestrrat committed Sep 15, 2020
1 parent 3651736 commit 4681c5b
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 8 deletions.
51 changes: 45 additions & 6 deletions clib/clib.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ package clib
#include <libxml/xpathInternals.h>
#include <libxml/c14n.h>
#include <libxml/xmlschemas.h>
#include <libxml/schemasInternals.h>
static inline void MY_nilErrorHandler(void *ctx, const char *msg, ...) {}
Expand Down Expand Up @@ -343,6 +344,7 @@ import (
"unsafe"

"github.com/lestrrat-go/libxml2/internal/debug"
"github.com/lestrrat-go/libxml2/internal/option"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -465,7 +467,14 @@ func ReportErrors(b bool) {
}

func xmlCtxtLastError(ctx PtrSource) error {
e := C.MY_xmlCtxtLastError(unsafe.Pointer(ctx.Pointer()))
return xmlCtxtLastErrorRaw(ctx.Pointer())
}

func xmlCtxtLastErrorRaw(ctx uintptr) error {
e := C.MY_xmlCtxtLastError(unsafe.Pointer(ctx))
if e == nil {
return errors.New(`unknown error`)
}
msg := strings.TrimSuffix(C.GoString(e.message), "\n")
return errors.Errorf("Entity: line %v: parser error : %v", e.line, msg)
}
Expand Down Expand Up @@ -2116,11 +2125,41 @@ func XMLTextData(n PtrSource) string {
return xmlCharToString(nptr.content)
}

func XMLSchemaParse(buf []byte) (uintptr, error) {
parserCtx := C.xmlSchemaNewMemParserCtxt(
(*C.char)(unsafe.Pointer(&buf[0])),
C.int(len(buf)),
)
func XMLSchemaParse(buf []byte, options ...option.Interface) (uintptr, error) {
var uri string
var encoding string
var coptions int
for _, opt := range options {
switch opt.Name() {
case option.OptKeyWithURI:
uri = opt.Value().(string)
}
}

docctx := C.xmlCreateMemoryParserCtxt((*C.char)(unsafe.Pointer(&buf[0])), C.int(len(buf)))
if docctx == nil {
return 0, errors.New("error creating doc parser")
}

var curi *C.char
if uri != "" {
curi = C.CString(uri)
defer C.free(unsafe.Pointer(curi))
}

var cencoding *C.char
if encoding != "" {
cencoding = C.CString(encoding)
defer C.free(unsafe.Pointer(cencoding))
}

doc := C.xmlCtxtReadMemory(docctx, (*C.char)(unsafe.Pointer(&buf[0])), C.int(len(buf)), curi, cencoding, C.int(coptions))
if doc == nil {
return 0, errors.Errorf("failed to read schema from memory: %v",
xmlCtxtLastErrorRaw(uintptr(unsafe.Pointer(docctx))))
}

parserCtx := C.xmlSchemaNewDocParserCtxt((*C.xmlDoc)(unsafe.Pointer(doc)))
if parserCtx == nil {
return 0, errors.New("failed to create parser")
}
Expand Down
5 changes: 5 additions & 0 deletions internal/option/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package option

const (
OptKeyWithURI = `with-uri`
)
25 changes: 25 additions & 0 deletions internal/option/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package option

type Interface interface {
Name() string
Value() interface{}
}

type Option struct {
name string
value interface{}
}

func New(name string, value interface{}) *Option {
return &Option{
name: name,
value: value,
}
}

func (o *Option) Name() string {
return o.name
}
func (o *Option) Value() interface{} {
return o.value
}
31 changes: 31 additions & 0 deletions test/schema/projects/go_libxml2_remote.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema targetNamespace="http://xmlsoft.org/"
xmlns="http://xmlsoft.org/"
xmlns:libxml2="http://xmlsoft.org/"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
attributeFormDefault="unqualified">


<xs:include schemaLocation="../lib/types/cksum.xsd"/>
<xs:include schemaLocation="../lib/types/std.xsd"/>

<xs:element name="libxml2">
<xs:complexType>
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="sub">
<xs:complexType>
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="sub2" type="xs:string" minOccurs="0"/>
<xs:element name="nestedInclude" type="t_cksum_hash" minOccurs="1" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="attrOpt" use="optional" default="none specified" type="xs:string"/>
<xs:attribute name="attrReq" use="required" type="t_std_nonempty"/>
<xs:attribute name="attrBoolNoDef" use="optional" type="xs:boolean"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>

</xs:schema>
4 changes: 4 additions & 0 deletions xsd/interface.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package xsd

import "github.com/lestrrat-go/libxml2/internal/option"

// Schema represents an XML schema.
type Schema struct {
ptr uintptr // *C.xmlSchema
Expand All @@ -11,3 +13,5 @@ type Schema struct {
type SchemaValidationError struct {
errors []error
}

type Option = option.Interface
41 changes: 41 additions & 0 deletions xsd/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package xsd

import (
"net/url"
"os"
"path/filepath"

"github.com/lestrrat-go/libxml2/internal/option"
)

// WithPath provides a hint to the XSD parser as to where the
// document being parsed is located at.
//
// This is useful when you must resolve relative paths inside a
// document, because to use relative paths the parser needs to
// know the reference location (i.e. location of the document
// being parsed). In case where you are parsing using `ParseFromFile()`
// this is handled automatically by the `ParseFromFile` method,
// but if you are using `Parse` method this is required
//
// If the path is provided as a relative path, the current directory
// should be obtainable via `os.Getwd` when this call is made, otherwise
// path resolution may fail in weird ways.
func WithPath(path string) Option {
if !filepath.IsAbs(path) {
if curdir, err := os.Getwd(); err == nil {
path = filepath.Join(curdir, path)
}
}

return WithURI(
(&url.URL{
Scheme: `file`,
Path: path,
}).String(),
)
}

func WithURI(v string) Option {
return option.New(option.OptKeyWithURI, v)
}
7 changes: 5 additions & 2 deletions xsd/xsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ const ValueVCCreate = 1
// Parse is used to parse an XML Schema Document to produce a
// Schema instance. Make sure to call Free() on the instance
// when you are done with it.
func Parse(buf []byte) (*Schema, error) {
sptr, err := clib.XMLSchemaParse(buf)


func Parse(buf []byte, options ...Option) (*Schema, error) {
// xsd.WithURI(...)
sptr, err := clib.XMLSchemaParse(buf, options...)
if err != nil {
return nil, errors.Wrap(err, "failed to parse input")
}
Expand Down
81 changes: 81 additions & 0 deletions xsd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package libxml2_test

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -162,3 +164,82 @@ elementFormDefault="qualified">
t.Logf("%s", doc.String())

}

func TestGHIssue67(t *testing.T) {
t.Run("Local validation", func(t *testing.T) {
const schemafile = "test/schema/projects/go_libxml2_local.xsd"
const docfile = "test/go_libxml2_local.xml"

schemasrc, err := ioutil.ReadFile(schemafile)
if !assert.NoError(t, err, `failed to read xsd file`) {
return
}

docsrc, err := ioutil.ReadFile(docfile)
if !assert.NoError(t, err, `failed to read xml file`) {
return
}

schema, err := xsd.Parse(schemasrc, xsd.WithPath(schemafile))
if !assert.NoError(t, err, `xsd.Parse should succeed`) {
return
}
defer schema.Free()

doc, err := libxml2.Parse(docsrc)
if !assert.NoError(t, err, "parsing XML") {
return
}
defer doc.Free()
if !assert.NoError(t, schema.Validate(doc, xsd.ValueVCCreate), `schema.Validate should succeed`) {
return
}

t.Logf("%s", doc.String())
})
t.Run("Remote validation", func(t *testing.T) {
curdir, err := os.Getwd()
if !assert.NoError(t, err, `os.Getwd failed`) {
return
}

srv := httptest.NewServer(http.FileServer(http.Dir(curdir)))
defer srv.Close()

var schemafile = srv.URL + "/test/schema/projects/go_libxml2_remote.xsd"
const docfile = "test/go_libxml2_remote.xml"

res, err := http.Get(schemafile)
if !assert.NoError(t, err, `failed to fetch xsd file`) {
return
}

schemasrc, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if !assert.NoError(t, err, `failed to read xsd file`) {
return
}

docsrc, err := ioutil.ReadFile(docfile)
if !assert.NoError(t, err, `failed to read xml file`) {
return
}

schema, err := xsd.Parse(schemasrc, xsd.WithURI(schemafile))
if !assert.NoError(t, err, `xsd.Parse should succeed`) {
return
}
defer schema.Free()

doc, err := libxml2.Parse(docsrc)
if !assert.NoError(t, err, "parsing XML") {
return
}
defer doc.Free()
if !assert.NoError(t, schema.Validate(doc, xsd.ValueVCCreate), `schema.Validate should succeed`) {
return
}

t.Logf("%s", doc.String())
})
}

0 comments on commit 4681c5b

Please sign in to comment.