Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 0e3bc32

Browse files
Fix CRLF detection on single-file app + fix YAML documents order detection for single-file apps
- Split and Merge remembers the CRLF detection while outputting the result Signed-off-by: Silvin Lubecki <[email protected]>
1 parent cfc0281 commit 0e3bc32

File tree

5 files changed

+100
-41
lines changed

5 files changed

+100
-41
lines changed

internal/packager/split.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ func Merge(app *types.App, target io.Writer) error {
4545
}
4646
for _, data := range [][]byte{
4747
app.MetadataRaw(),
48-
[]byte(types.SingleFileSeparator),
48+
types.YamlSingleFileSeparator(app.HasCRLF()),
4949
app.Composes()[0],
50-
[]byte(types.SingleFileSeparator),
50+
types.YamlSingleFileSeparator(app.HasCRLF()),
5151
app.ParametersRaw()[0],
5252
} {
5353
if _, err := target.Write(data); err != nil {

loader/loader.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package loader
22

33
import (
4+
"bytes"
45
"io"
56
"io/ioutil"
67
"os"
78
"path/filepath"
8-
"strings"
9+
10+
"github.com/docker/cli/cli/compose/loader"
11+
12+
"github.com/docker/app/specification"
13+
"github.com/docker/cli/cli/compose/schema"
914

1015
"github.com/docker/app/internal"
1116
"github.com/docker/app/types"
@@ -19,20 +24,38 @@ func LoadFromSingleFile(path string, r io.Reader, ops ...func(*types.App) error)
1924
if err != nil {
2025
return nil, errors.Wrap(err, "error reading single-file")
2126
}
22-
parts := strings.Split(string(data), types.SingleFileSeparator)
27+
hasCRLF := bytes.Contains(data, []byte{'\r', '\n'})
28+
29+
parts := bytes.Split(data, types.YamlSingleFileSeparator(hasCRLF))
2330
if len(parts) != 3 {
2431
return nil, errors.Errorf("malformed single-file application: expected 3 documents, got %d", len(parts))
2532
}
26-
// 0. is metadata
27-
metadata := strings.NewReader(parts[0])
28-
// 1. is compose
29-
compose := strings.NewReader(parts[1])
30-
// 2. is parameters
31-
parameters := strings.NewReader(parts[2])
33+
34+
var (
35+
metadata io.Reader
36+
compose io.Reader
37+
params io.Reader
38+
)
39+
for i := 0; i < 3; i++ {
40+
parsed, err := loader.ParseYAML(parts[i])
41+
if err != nil {
42+
return nil, err
43+
}
44+
if err := specification.Validate(parsed, internal.MetadataVersion); metadata == nil && err == nil {
45+
metadata = bytes.NewBuffer(parts[i])
46+
} else if err2 := schema.Validate(parsed, schema.Version(parsed)); compose == nil && err2 == nil {
47+
compose = bytes.NewBuffer(parts[i])
48+
} else if params == nil {
49+
params = bytes.NewBuffer(parts[i])
50+
} else {
51+
return nil, errors.New("malformed single-file application")
52+
}
53+
}
3254
appOps := append([]func(*types.App) error{
3355
types.WithComposes(compose),
34-
types.WithParameters(parameters),
56+
types.WithParameters(params),
3557
types.Metadata(metadata),
58+
types.WithCRLF(hasCRLF),
3659
}, ops...)
3760
return types.NewApp(path, appOps...)
3861
}

loader/loader_test.go

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,44 @@ import (
1818

1919
const (
2020
metadata = `name: my-app
21-
version: 1.0.0`
22-
yaml = `version: "3.1"
23-
24-
services:
21+
version: 1.0.0
22+
`
23+
compose = `services:
2524
web:
26-
image: nginx`
27-
parameters = `foo: bar`
25+
image: nginx
26+
version: "3.1"
27+
`
28+
params = `foo: bar
29+
`
2830
)
2931

3032
func TestLoadFromSingleFile(t *testing.T) {
31-
singlefile := fmt.Sprintf(`%s
32-
---
33-
%s
34-
---
35-
%s`, metadata, yaml, parameters)
36-
app, err := LoadFromSingleFile("my-app", strings.NewReader(singlefile))
37-
assert.NilError(t, err)
38-
assert.Assert(t, app != nil)
39-
assert.Assert(t, is.Equal(app.Path, "my-app"))
40-
assertAppContent(t, app)
33+
testCases := []struct {
34+
name string
35+
file string
36+
}{
37+
{
38+
name: "line-feed",
39+
file: fmt.Sprintf("%s\n---\n%s\n---\n%s", metadata, compose, params),
40+
},
41+
{
42+
name: "carriage-return-line-feed",
43+
file: fmt.Sprintf("%s\r\n---\r\n%s\r\n---\r\n%s", metadata, compose, params),
44+
},
45+
{
46+
name: "unordered-documents",
47+
file: fmt.Sprintf("%s\n---\n%s\n---\n%s", params, metadata, compose),
48+
},
49+
}
50+
for _, test := range testCases {
51+
t.Run(test.name, func(t *testing.T) {
52+
app, err := LoadFromSingleFile("my-app", strings.NewReader(test.file))
53+
assert.NilError(t, err)
54+
assert.Assert(t, app != nil)
55+
assert.Assert(t, is.Equal(app.Path, "my-app"))
56+
assertAppContent(t, app)
57+
})
58+
}
4159
}
4260

4361
func TestLoadFromSingleFileInvalidReader(t *testing.T) {
@@ -46,17 +64,17 @@ func TestLoadFromSingleFileInvalidReader(t *testing.T) {
4664
}
4765

4866
func TestLoadFromSingleFileMalformed(t *testing.T) {
49-
_, err := LoadFromSingleFile("my-app", strings.NewReader(`foo
67+
_, err := LoadFromSingleFile("my-app", strings.NewReader(`foo: foo
5068
---
51-
bar`))
69+
bar: bar`))
5270
assert.ErrorContains(t, err, "malformed single-file application")
5371
}
5472

5573
func TestLoadFromDirectory(t *testing.T) {
5674
dir := fs.NewDir(t, "my-app",
5775
fs.WithFile(internal.MetadataFileName, metadata),
58-
fs.WithFile(internal.ParametersFileName, parameters),
59-
fs.WithFile(internal.ComposeFileName, yaml),
76+
fs.WithFile(internal.ParametersFileName, params),
77+
fs.WithFile(internal.ComposeFileName, compose),
6078
)
6179
defer dir.Remove()
6280
app, err := LoadFromDirectory(dir.Path())
@@ -69,8 +87,8 @@ func TestLoadFromDirectory(t *testing.T) {
6987
func TestLoadFromDirectoryDeprecatedSettings(t *testing.T) {
7088
dir := fs.NewDir(t, "my-app",
7189
fs.WithFile(internal.MetadataFileName, metadata),
72-
fs.WithFile(internal.DeprecatedSettingsFileName, parameters),
73-
fs.WithFile(internal.ComposeFileName, yaml),
90+
fs.WithFile(internal.DeprecatedSettingsFileName, params),
91+
fs.WithFile(internal.ComposeFileName, compose),
7492
)
7593
defer dir.Remove()
7694
app, err := LoadFromDirectory(dir.Path())
@@ -97,8 +115,8 @@ func createAppTar(t *testing.T) *fs.File {
97115
t.Helper()
98116
dir := fs.NewDir(t, "my-app",
99117
fs.WithFile(internal.MetadataFileName, metadata),
100-
fs.WithFile(internal.ParametersFileName, parameters),
101-
fs.WithFile(internal.ComposeFileName, yaml),
118+
fs.WithFile(internal.ParametersFileName, params),
119+
fs.WithFile(internal.ComposeFileName, compose),
102120
)
103121
defer dir.Remove()
104122
r, err := archive.TarWithOptions(dir.Path(), &archive.TarOptions{
@@ -117,9 +135,9 @@ func assertContentIs(t *testing.T, actual []byte, expected string) {
117135

118136
func assertAppContent(t *testing.T, app *types.App) {
119137
assert.Assert(t, is.Len(app.ParametersRaw(), 1))
120-
assertContentIs(t, app.ParametersRaw()[0], parameters)
138+
assertContentIs(t, app.ParametersRaw()[0], params)
121139
assert.Assert(t, is.Len(app.Composes(), 1))
122-
assertContentIs(t, app.Composes()[0], yaml)
140+
assertContentIs(t, app.Composes()[0], compose)
123141
assertContentIs(t, app.MetadataRaw(), metadata)
124142
}
125143

specification/schema.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,4 @@ func Validate(config map[string]interface{}, version string) error {
3636
}
3737

3838
return nil
39-
4039
}

types/types.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package types
22

33
import (
4+
"bytes"
45
"errors"
56
"io"
67
"io/ioutil"
@@ -13,9 +14,6 @@ import (
1314
"github.com/docker/app/types/parameters"
1415
)
1516

16-
// SingleFileSeparator is the separator used in single-file app
17-
const SingleFileSeparator = "\n---\n"
18-
1917
// AppSourceKind represents what format the app was in when read
2018
type AppSourceKind int
2119

@@ -30,6 +28,14 @@ const (
3028
AppSourceArchive
3129
)
3230

31+
// YamlSingleFileSeparator returns the separator used in single-file app, depending detected CRLF
32+
func YamlSingleFileSeparator(hasCRLF bool) []byte {
33+
if hasCRLF {
34+
return []byte("\r\n---\r\n")
35+
}
36+
return []byte("\n---\n")
37+
}
38+
3339
// ShouldRunInsideDirectory returns whether the package is run from a directory on disk
3440
func (a AppSourceKind) ShouldRunInsideDirectory() bool {
3541
return a == AppSourceSplit || a == AppSourceImage || a == AppSourceArchive
@@ -48,6 +54,7 @@ type App struct {
4854
metadataContent []byte
4955
metadata metadata.AppMetadata
5056
attachments []Attachment
57+
hasCRLF bool
5158
}
5259

5360
// Attachment is a summary of an attachment (attached file) stored in the app definition
@@ -96,6 +103,10 @@ func (a *App) Attachments() []Attachment {
96103
return a.attachments
97104
}
98105

106+
func (a *App) HasCRLF() bool {
107+
return a.hasCRLF
108+
}
109+
99110
// Extract writes the app in the specified folder
100111
func (a *App) Extract(path string) error {
101112
if err := ioutil.WriteFile(filepath.Join(path, internal.MetadataFileName), a.MetadataRaw(), 0644); err != nil {
@@ -177,6 +188,13 @@ func WithSource(source AppSourceKind) func(*App) error {
177188
}
178189
}
179190

191+
func WithCRLF(hasCRLF bool) func(*App) error {
192+
return func(app *App) error {
193+
app.hasCRLF = hasCRLF
194+
return nil
195+
}
196+
}
197+
180198
// WithParametersFiles adds the specified parameters files to the app
181199
func WithParametersFiles(files ...string) func(*App) error {
182200
return parametersLoader(func() ([][]byte, error) { return readFiles(files...) })
@@ -258,6 +276,7 @@ func metadataLoader(f func() ([]byte, error)) func(app *App) error {
258276
}
259277
app.metadata = loaded
260278
app.metadataContent = d
279+
app.hasCRLF = bytes.Contains(d, []byte{'\r', '\n'})
261280
return nil
262281
}
263282
}

0 commit comments

Comments
 (0)