• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-artifact / 1696787489

03 Mar 2025 12:08PM UTC coverage: 76.611%. Remained the same
1696787489

push

gitlab-ci

web-flow
Merge pull request #681 from lluiscampos/deprecate-ioutils

chore: Update the code away from deprecated `io/ioutils`

27 of 29 new or added lines in 10 files covered. (93.1%)

2 existing lines in 1 file now uncovered.

5909 of 7713 relevant lines covered (76.61%)

132.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.76
/cli/partition.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package cli
16

17
import (
18
        "bytes"
19
        "fmt"
20
        "io"
21
        "os"
22
        "os/exec"
23
        "path/filepath"
24
        "regexp"
25
        "strconv"
26
        "strings"
27
        "syscall"
28

29
        "github.com/pkg/errors"
30

31
        "github.com/mendersoftware/mender-artifact/areader"
32
        "github.com/mendersoftware/mender-artifact/artifact"
33
        "github.com/mendersoftware/mender-artifact/utils"
34
)
35

36
const (
37
        fat = iota
38
        ext
39
        unsupported
40

41
        // empty placeholder, so that we can write virtualImage.Open()
42
        // as the API call (better semantics).
43
        virtualImage vImage = 1
44
)
45

46
var errFsTypeUnsupported = errors.New("mender-artifact can only modify ext4 and vfat payloads")
47
var errBlkidNotFound = errors.New("`blkid` binary not found on the system")
48

49
type VPImage interface {
50
        io.Closer
51
        Open(fpath string) (VPFile, error)
52
        OpenDir(fpath string) (VPDir, error)
53
        dirtyImage()
54
}
55

56
// V(irtual)P(artition)File mimicks a file in an Artifact or on an sdimg.
57
type VPFile interface {
58
        io.ReadWriteCloser
59
        Delete(recursive bool) error
60
        CopyTo(hostFile string) error
61
        CopyFrom(hostFile string) error
62
}
63

64
// VPDir V(irtual)P(artition)Dir mimics a directory in an Artifact or on an sdimg.
65
type VPDir interface {
66
        io.Closer
67
        Create() error
68
}
69

70
type partition struct {
71
        offset string
72
        size   string
73
        path   string
74
}
75

76
type ModImageBase struct {
77
        path  string
78
        dirty bool
79
}
80

81
type ModImageArtifact struct {
82
        ModImageBase
83
        *unpackedArtifact
84
        comp artifact.Compressor
85
        key  SigningKey
86
}
87

88
type ModImageSdimg struct {
89
        ModImageBase
90
        candidates []partition
91
}
92

93
type ModImageRaw struct {
94
        ModImageBase
95
}
96

97
type vImage int
98

99
type vImageAndFile struct {
100
        image VPImage
101
        file  VPFile
102
}
103

104
type vImageAndDir struct {
105
        image VPImage
106
        dir   VPDir
107
}
108

109
// Open is a utility function that parses an input image and returns a
110
// V(irtual)P(artition)Image.
111
func (v vImage) Open(
112
        key SigningKey,
113
        imgname string,
114
        overrideCompressor ...artifact.Compressor,
115
) (VPImage, error) {
242✔
116
        // first we need to check  if we are having artifact or image file
242✔
117
        art, err := os.Open(imgname)
242✔
118
        if err != nil {
242✔
119
                return nil, errors.Wrap(err, "can not open artifact")
×
120
        }
×
121
        defer art.Close()
242✔
122

242✔
123
        aReader := areader.NewReader(art)
242✔
124
        err = aReader.ReadArtifact()
242✔
125
        if err == nil {
352✔
126
                // we have VALID artifact,
110✔
127

110✔
128
                // First check if it is a module-image type
110✔
129
                inst := aReader.GetHandlers()
110✔
130

110✔
131
                if len(inst) > 1 {
110✔
132
                        return nil, errors.New(
×
133
                                "Modifying artifacts with more than one payload is not supported",
×
134
                        )
×
135
                }
×
136

137
                unpackedArtifact, err := unpackArtifact(imgname)
110✔
138
                if err != nil {
110✔
139
                        return nil, errors.Wrap(err, "can not process artifact")
×
140
                }
×
141

142
                var comp artifact.Compressor
110✔
143
                if len(overrideCompressor) == 1 {
112✔
144
                        comp = overrideCompressor[0]
2✔
145
                } else {
110✔
146
                        comp = unpackedArtifact.ar.Compressor()
108✔
147
                }
108✔
148

149
                return &ModImageArtifact{
110✔
150
                        ModImageBase: ModImageBase{
110✔
151
                                path:  imgname,
110✔
152
                                dirty: false,
110✔
153
                        },
110✔
154
                        unpackedArtifact: unpackedArtifact,
110✔
155
                        comp:             comp,
110✔
156
                        key:              key,
110✔
157
                }, nil
110✔
158
        } else {
132✔
159
                return processSdimg(imgname)
132✔
160
        }
132✔
161
}
162

163
// Shortcut to use an image with one file. This is inefficient if you are going
164
// to write more than one file, since it writes out the entire image
165
// afterwards. In that case use VPImage and VPFile instead.
166
func (v vImage) OpenFile(key SigningKey, imgAndPath string) (VPFile, error) {
173✔
167
        imagepath, filepath, err := parseImgPath(imgAndPath)
173✔
168
        if err != nil {
177✔
169
                return nil, err
4✔
170
        }
4✔
171

172
        image, err := v.Open(key, imagepath)
169✔
173
        if err != nil {
169✔
174
                return nil, err
×
175
        }
×
176

177
        file, err := image.Open(filepath)
169✔
178
        if err != nil {
195✔
179
                image.Close()
26✔
180
                return nil, err
26✔
181
        }
26✔
182

183
        return &vImageAndFile{
143✔
184
                image: image,
143✔
185
                file:  file,
143✔
186
        }, nil
143✔
187
}
188

189
// Shortcut to use an image with one directory.
190
func (v vImage) OpenDir(key SigningKey, imgAndPath string) (VPDir, error) {
15✔
191
        imagepath, dirpath, err := parseImgPath(imgAndPath)
15✔
192
        if err != nil {
15✔
193
                return nil, err
×
194
        }
×
195

196
        image, err := v.Open(key, imagepath)
15✔
197
        if err != nil {
15✔
198
                return nil, err
×
199
        }
×
200

201
        dir, err := image.OpenDir(dirpath)
15✔
202
        if err != nil {
15✔
203
                image.Close()
×
204
                return nil, err
×
205
        }
×
206

207
        return &vImageAndDir{
15✔
208
                image: image,
15✔
209
                dir:   dir,
15✔
210
        }, nil
15✔
211
}
212

213
// Shortcut to open a file in the image, write into it, and close it again.
214
func CopyIntoImage(hostFile string, image VPImage, imageFile string) error {
29✔
215
        imageFd, err := image.Open(imageFile)
29✔
216
        if err != nil {
37✔
217
                return err
8✔
218
        }
8✔
219

220
        err = imageFd.CopyTo(hostFile)
21✔
221
        if err != nil {
21✔
222
                imageFd.Close()
×
223
                return err
×
224
        }
×
225
        return imageFd.Close()
21✔
226
}
227

228
// Shortcut to open a file in the image, read from it, and close it again.
229
func CopyFromImage(image VPImage, imageFile string, hostFile string) error {
8✔
230
        imageFd, err := image.Open(imageFile)
8✔
231
        if err != nil {
12✔
232
                return err
4✔
233
        }
4✔
234

235
        err = imageFd.CopyFrom(hostFile)
4✔
236
        if err != nil {
4✔
237
                imageFd.Close()
×
238
                return err
×
239
        }
×
240
        return imageFd.Close()
4✔
241
}
242

243
func (v *vImageAndFile) Read(buf []byte) (int, error) {
16✔
244
        return v.file.Read(buf)
16✔
245
}
16✔
246

247
func (v *vImageAndFile) Write(buf []byte) (int, error) {
4✔
248
        v.image.dirtyImage()
4✔
249
        return v.file.Write(buf)
4✔
250
}
4✔
251

252
func (v *vImageAndFile) Delete(recursive bool) error {
16✔
253
        v.image.dirtyImage()
16✔
254
        return v.file.Delete(recursive)
16✔
255
}
16✔
256

257
func (v *vImageAndFile) CopyTo(hostFile string) error {
67✔
258
        v.image.dirtyImage()
67✔
259
        return v.file.CopyTo(hostFile)
67✔
260
}
67✔
261

262
func (v *vImageAndFile) CopyFrom(hostFile string) error {
20✔
263
        return v.file.CopyFrom(hostFile)
20✔
264
}
20✔
265

266
func (v *vImageAndFile) Close() error {
143✔
267
        fileErr := v.file.Close()
143✔
268
        imageErr := v.image.Close()
143✔
269

143✔
270
        if fileErr != nil {
144✔
271
                return fileErr
1✔
272
        } else {
143✔
273
                return imageErr
142✔
274
        }
142✔
275
}
276

277
func (v *vImageAndDir) Create() error {
15✔
278
        v.image.dirtyImage()
15✔
279
        return v.dir.Create()
15✔
280
}
15✔
281

282
func (v *vImageAndDir) Close() error {
15✔
283
        dirErr := v.dir.Close()
15✔
284
        imageErr := v.image.Close()
15✔
285

15✔
286
        if dirErr != nil {
15✔
287
                return dirErr
×
288
        } else {
15✔
289
                return imageErr
15✔
290
        }
15✔
291
}
292

293
// Opens a file inside the image(s) represented by the ModImageArtifact
294
func (i *ModImageArtifact) Open(fpath string) (VPFile, error) {
90✔
295
        return newArtifactExtFile(i, i.comp, fpath, i.files[0])
90✔
296
}
90✔
297

298
// Opens a dir inside the image(s) represented by the ModImageArtifact
299
func (i *ModImageArtifact) OpenDir(fpath string) (VPDir, error) {
6✔
300
        return newArtifactExtDir(i, i.comp, fpath, i.files[0])
6✔
301
}
6✔
302

303
// Closes and repacks the artifact or sdimg.
304
func (i *ModImageArtifact) Close() error {
110✔
305
        if i.unpackDir != "" {
220✔
306
                defer os.RemoveAll(i.unpackDir)
110✔
307
        }
110✔
308
        if i.dirty {
186✔
309
                return repackArtifact(i.comp, i.key, i.unpackedArtifact)
76✔
310
        }
76✔
311
        return nil
34✔
312
}
313

314
func (i *ModImageArtifact) dirtyImage() {
76✔
315
        i.dirty = true
76✔
316
}
76✔
317

318
// Opens a file inside the image(s) represented by the ModImageSdimg
319
func (i *ModImageSdimg) Open(fpath string) (VPFile, error) {
112✔
320
        return newSDImgFile(i, fpath, i.candidates)
112✔
321
}
112✔
322

323
func (i *ModImageSdimg) OpenDir(fpath string) (VPDir, error) {
9✔
324
        return newSDImgDir(i, fpath, i.candidates)
9✔
325
}
9✔
326

327
func (i *ModImageSdimg) Close() error {
119✔
328
        for _, cand := range i.candidates {
595✔
329
                if cand.path != "" && cand.path != i.path {
952✔
330
                        defer os.RemoveAll(cand.path)
476✔
331
                }
476✔
332
        }
333
        if i.dirty {
188✔
334
                return repackSdimg(i.candidates, i.path)
69✔
335
        }
69✔
336
        return nil
50✔
337
}
338

339
func (i *ModImageSdimg) dirtyImage() {
69✔
340
        i.dirty = true
69✔
341
}
69✔
342

343
func (i *ModImageRaw) Open(fpath string) (VPFile, error) {
4✔
344
        return newExtFile(i.path, fpath)
4✔
345
}
4✔
346

347
func (i *ModImageRaw) OpenDir(fpath string) (VPDir, error) {
×
348
        return newExtDir(i.path, fpath)
×
349
}
×
350

351
func (i *ModImageRaw) Close() error {
12✔
352
        return nil
12✔
353
}
12✔
354

355
func (i *ModImageRaw) dirtyImage() {
12✔
356
        i.dirty = true
12✔
357
}
12✔
358

359
// parseImgPath parses cli input of the form
360
// path/to/[sdimg,mender]:/path/inside/img/file
361
// into path/to/[sdimg,mender] and path/inside/img/file
362
func parseImgPath(imgpath string) (imgname, fpath string, err error) {
188✔
363
        paths := strings.SplitN(imgpath, ":", 2)
188✔
364
        if len(paths) != 2 {
190✔
365
                return "", "", fmt.Errorf("failed to parse image path %q", imgpath)
2✔
366
        }
2✔
367
        if len(paths[1]) == 0 {
188✔
368
                return "", "", errors.New("please enter a path into the image")
2✔
369
        }
2✔
370
        return paths[0], paths[1], nil
184✔
371
}
372

373
// imgFilesystemtype returns the filesystem type of a partition.
374
// Currently only distinguishes ext from fat.
375
func imgFilesystemType(imgpath string) (int, error) {
175✔
376
        bin, err := utils.GetBinaryPath("blkid")
175✔
377
        if err != nil {
177✔
378
                return unsupported, errBlkidNotFound
2✔
379
        }
2✔
380
        cmd := exec.Command(bin, "-s", "TYPE", imgpath)
173✔
381
        buf := bytes.NewBuffer(nil)
173✔
382
        cmd.Stdout = buf
173✔
383
        if err := cmd.Run(); err != nil {
173✔
384
                return unsupported, errors.Wrap(err, "imgFilesystemType: blkid command failed")
×
385
        }
×
386
        if strings.Contains(buf.String(), `TYPE="vfat"`) {
188✔
387
                return fat, nil
15✔
388
        } else if strings.Contains(buf.String(), `TYPE="ext`) {
331✔
389
                return ext, nil
158✔
390
        }
158✔
391
        return unsupported, nil
×
392
}
393

394
// From the fsck man page:
395
// The exit code returned by fsck is the sum of the following conditions:
396
//
397
//        0      No errors
398
//        1      Filesystem errors corrected
399
//        2      System should be rebooted
400
//        4      Filesystem errors left uncorrected
401
//        8      Operational error
402
//        16     Usage or syntax error
403
//        32     Checking canceled by user request
404
//        128    Shared-library error
405
func runFsck(image, fstype string) error {
271✔
406
        bin, err := utils.GetBinaryPath("fsck." + fstype)
271✔
407
        if err != nil {
271✔
408
                return errors.Wrap(err, "fsck command not found")
×
409
        }
×
410
        cmd := exec.Command(bin, "-a", image)
271✔
411
        if err := cmd.Run(); err != nil {
316✔
412
                // try to get the exit code
45✔
413
                if exitError, ok := err.(*exec.ExitError); ok {
90✔
414
                        ws := exitError.Sys().(syscall.WaitStatus)
45✔
415
                        if ws.ExitStatus() == 0 || ws.ExitStatus() == 1 {
68✔
416
                                return nil
23✔
417
                        }
23✔
418
                        if ws.ExitStatus() == 8 {
44✔
419
                                return errFsTypeUnsupported
22✔
420
                        }
22✔
421
                        return errors.Wrap(err, "fsck error")
×
422
                }
423
                return errors.New("fsck returned unparsed error")
×
424
        }
425
        return nil
226✔
426
}
427

428
// sdimgFile is a virtual file for files on an sdimg.
429
// It can write and read to and from all partitions (boot,rootfsa,roofsb,data),
430
// where if a file is located on the rootfs, a write will be duplicated to both
431
// partitions.
432
type sdimgFile []VPFile
433

434
// sdimgDir is a virtual directory for files on an sdimg.
435
type sdimgDir []VPDir
436

437
func isSparsePartition(part partition) bool {
158✔
438
        // NOTE: Basically just checking for a filesystem
158✔
439
        _, err := debugfsExecuteCommand("stat /", part.path)
158✔
440
        return err != nil
158✔
441
}
158✔
442

443
// filterSparsePartitions returns partitions with data from an array of partitions
444
func filterSparsePartitions(parts []partition) []partition {
79✔
445
        ps := []partition{}
79✔
446
        for _, part := range parts {
237✔
447
                if isSparsePartition(part) {
181✔
448
                        continue
23✔
449
                }
450
                ps = append(ps, part)
135✔
451
        }
452
        return ps
79✔
453
}
454

455
// getFilesystems extracts only the partitions we want to modify.
456
// for {data,/[u]boot} this is one partition.
457
// for rootfs{a,b}, this is the two partitions (unless one of them is unpopulated,
458
// then only the one with data is returned)
459
func getFilesystems(fpath string, modcands []partition) ([]partition, string) {
121✔
460

121✔
461
        reg := regexp.MustCompile("/(uboot|boot/(efi|grub))[/]")
121✔
462

121✔
463
        var filesystems []partition
121✔
464
        if strings.HasPrefix(fpath, "/data") {
124✔
465
                // The data dir is not a directory in the data partition
3✔
466
                fpath = strings.TrimPrefix(fpath, "/data/")
3✔
467
                filesystems = append(filesystems, modcands[3])
3✔
468
        } else if reg.MatchString(fpath) {
160✔
469
                // /uboot, /boot/efi, /boot/grub are not directories on the boot partition.
39✔
470
                fpath = reg.ReplaceAllString(fpath, "")
39✔
471
                filesystems = append(filesystems, modcands[0])
39✔
472
        } else {
118✔
473
                filesystems = append(filesystems, filterSparsePartitions(modcands[1:3])...)
79✔
474
        }
79✔
475

476
        return filesystems, fpath
121✔
477
}
478

479
func newSDImgFile(image *ModImageSdimg, fpath string, modcands []partition) (sdimgFile, error) {
112✔
480
        if len(modcands) < 4 {
112✔
481
                return nil, fmt.Errorf("newSDImgFile: %d partitions found, 4 needed", len(modcands))
×
482
        }
×
483

484
        filesystems, pfpath := getFilesystems(fpath, modcands)
112✔
485

112✔
486
        // Since boot partitions can be either fat or ext, return a
112✔
487
        // readWriteCloser dependent upon the underlying filesystem type.
112✔
488
        var sdimgFile sdimgFile
112✔
489
        for _, fs := range filesystems {
270✔
490
                fstype, err := imgFilesystemType(fs.path)
158✔
491
                if err != nil {
158✔
492
                        return nil, errors.Wrap(err, "partition: error reading file-system type on partition")
×
493
                }
×
494
                var f VPFile
158✔
495
                switch fstype {
158✔
496
                case fat:
15✔
497
                        f, err = newFatFile(fs.path, pfpath)
15✔
498
                case ext:
143✔
499
                        f, err = newExtFile(fs.path, pfpath)
143✔
500
                case unsupported:
×
501
                        err = errors.New("partition: unsupported filesystem")
×
502

503
                }
504
                if err != nil {
166✔
505
                        sdimgFile.Close()
8✔
506
                        return nil, err
8✔
507
                }
8✔
508
                sdimgFile = append(sdimgFile, f)
150✔
509
        }
510
        return sdimgFile, nil
104✔
511
}
512

513
func newSDImgDir(image *ModImageSdimg, fpath string, modcands []partition) (sdimgDir, error) {
9✔
514
        if len(modcands) < 4 {
9✔
515
                return nil, fmt.Errorf("newSDImgDir: %d partitions found, 4 needed", len(modcands))
×
516
        }
×
517

518
        filesystems, pfpath := getFilesystems(fpath, modcands)
9✔
519

9✔
520
        // Since boot partitions can be either fat or ext, return a
9✔
521
        // Closer dependent upon the underlying filesystem type.
9✔
522
        var sdimgDir sdimgDir
9✔
523
        for _, fs := range filesystems {
24✔
524
                fstype, err := imgFilesystemType(fs.path)
15✔
525
                if err != nil {
15✔
526
                        return nil, errors.Wrap(err, "partition: error reading file-system type on partition")
×
527
                }
×
528
                var d VPDir
15✔
529
                switch fstype {
15✔
530
                case fat:
×
531
                        d, err = newFatDir(fs.path, pfpath)
×
532
                case ext:
15✔
533
                        d, err = newExtDir(fs.path, pfpath)
15✔
534
                case unsupported:
×
535
                        err = errors.New("partition: unsupported filesystem")
×
536

537
                }
538
                if err != nil {
15✔
539
                        sdimgDir.Close()
×
540
                        return nil, err
×
541
                }
×
542
                sdimgDir = append(sdimgDir, d)
15✔
543
        }
544
        return sdimgDir, nil
9✔
545
}
546

547
// Write forces a write from the underlying writers sdimgFile wraps.
548
func (p sdimgFile) Write(b []byte) (int, error) {
4✔
549
        for _, part := range p {
8✔
550
                n, err := part.Write(b)
4✔
551
                if err != nil {
4✔
552
                        return n, err
×
553
                }
×
554
                if n != len(b) {
4✔
555
                        return n, io.ErrShortWrite
×
556
                }
×
557
        }
558
        return len(b), nil
4✔
559
}
560

561
// Read reads a file from an sdimg.
562
func (p sdimgFile) Read(b []byte) (int, error) {
10✔
563
        // A read from the first partition wrapped should suffice in all cases.
10✔
564
        if len(p) == 0 {
10✔
565
                return 0, errors.New("No partition set to read from")
×
566
        }
×
567
        return p[0].Read(b)
10✔
568
}
569

570
func (p sdimgFile) CopyTo(hostFile string) error {
47✔
571
        for _, part := range p {
116✔
572
                err := part.CopyTo(hostFile)
69✔
573
                if err != nil {
69✔
574
                        return err
×
575
                }
×
576
        }
577
        return nil
47✔
578
}
579

580
func (p sdimgFile) CopyFrom(hostFile string) error {
21✔
581
        if len(p) == 0 {
21✔
582
                return errors.New("No partition set to copy from")
×
583
        }
×
584
        return p[0].CopyFrom(hostFile)
21✔
585
}
586

587
// Read reads a file from an sdimg.
588
func (p sdimgFile) Delete(recursive bool) (err error) {
10✔
589
        for _, part := range p {
24✔
590
                err = part.Delete(recursive)
14✔
591
                if err != nil {
16✔
592
                        return err
2✔
593
                }
2✔
594
        }
595
        return nil
8✔
596
}
597

598
// Close closes the underlying closers.
599
func (p sdimgFile) Close() (err error) {
112✔
600
        if p == nil {
120✔
601
                return nil
8✔
602
        }
8✔
603
        for _, part := range p {
254✔
604
                err = part.Close()
150✔
605
                if err != nil {
151✔
606
                        return err
1✔
607
                }
1✔
608
        }
609
        return nil
103✔
610
}
611

612
func (p sdimgDir) Create() (err error) {
9✔
613
        for _, part := range p {
24✔
614
                err := part.Create()
15✔
615
                if err != nil {
15✔
616
                        return err
×
617
                }
×
618
        }
619
        return nil
9✔
620
}
621

622
// Close closes the underlying closers.
623
func (p sdimgDir) Close() (err error) {
9✔
624
        if p == nil {
9✔
625
                return nil
×
626
        }
×
627
        for _, part := range p {
24✔
628
                err = part.Close()
15✔
629
                if err != nil {
15✔
630
                        return err
×
631
                }
×
632
        }
633
        return nil
9✔
634
}
635

636
func newArtifactExtFile(
637
        image *ModImageArtifact,
638
        comp artifact.Compressor,
639
        fpath,
640
        imgpath string,
641
) (VPFile, error) {
90✔
642
        reg := regexp.MustCompile("/(uboot|boot/(efi|grub))")
90✔
643
        if reg.MatchString(fpath) {
94✔
644
                return nil, errors.New(
4✔
645
                        "newArtifactExtFile: A mender artifact does not contain a boot partition," +
4✔
646
                                " only a rootfs",
4✔
647
                )
4✔
648
        }
4✔
649
        if strings.HasPrefix(fpath, "/data") {
88✔
650
                return nil, errors.New(
2✔
651
                        "newArtifactExtFile: A mender artifact does not contain a data partition," +
2✔
652
                                " only a rootfs",
2✔
653
                )
2✔
654
        }
2✔
655

656
        return newExtFile(imgpath, fpath)
84✔
657
}
658

659
func newArtifactExtDir(
660
        image *ModImageArtifact,
661
        comp artifact.Compressor,
662
        fpath string,
663
        imgpath string,
664
) (VPDir, error) {
6✔
665
        reg := regexp.MustCompile("/(uboot|boot/(efi|grub))")
6✔
666
        if reg.MatchString(fpath) {
6✔
667
                return nil, errors.New(
×
668
                        "newArtifactExtDir: A mender artifact does not contain a boot partition, only a rootfs",
×
669
                )
×
670
        }
×
671
        if strings.HasPrefix(fpath, "/data") {
6✔
672
                return nil, errors.New(
×
673
                        "newArtifactExtDir: A mender artifact does not contain a data partition, only a rootfs",
×
674
                )
×
675
        }
×
676

677
        return newExtDir(imgpath, fpath)
6✔
678
}
679

680
// extFile wraps partition and implements ReadWriteCloser
681
type extFile struct {
682
        imagePath     string
683
        imageFilePath string
684
        flush         bool     // True if Close() needs to copy the file to the image
685
        tmpf          *os.File // Used as a buffer for multiple write operations
686
}
687

688
// extDir wraps partition
689
type extDir struct {
690
        imagePath     string
691
        imageFilePath string
692
}
693

694
func newExtFile(imagePath, imageFilePath string) (e *extFile, err error) {
231✔
695
        if err := runFsck(imagePath, "ext4"); err != nil {
251✔
696
                return nil, err
20✔
697
        }
20✔
698

699
        // Check that the given directory exists.
700
        _, err = debugfsExecuteCommand(fmt.Sprintf("cd %s", filepath.Dir(imageFilePath)), imagePath)
211✔
701
        if err != nil {
223✔
702
                return nil, fmt.Errorf(
12✔
703
                        "The directory: %s does not exist in the image", filepath.Dir(imageFilePath),
12✔
704
                )
12✔
705
        }
12✔
706
        tmpf, err := os.CreateTemp("", "mendertmp-extfile")
199✔
707
        // Cleanup resources in case of error.
199✔
708
        e = &extFile{
199✔
709
                imagePath:     imagePath,
199✔
710
                imageFilePath: imageFilePath,
199✔
711
                tmpf:          tmpf,
199✔
712
        }
199✔
713
        return e, err
199✔
714
}
715

716
func newExtDir(imagePath, imageFilePath string) (e *extDir, err error) {
21✔
717
        if err := runFsck(imagePath, "ext4"); err != nil {
21✔
718
                return nil, err
×
719
        }
×
720

721
        // Cleanup resources in case of error.
722
        e = &extDir{
21✔
723
                imagePath:     imagePath,
21✔
724
                imageFilePath: imageFilePath,
21✔
725
        }
21✔
726
        return e, err
21✔
727
}
728

729
// Write reads all bytes from b into the partitionFile using debugfs.
730
func (ef *extFile) Write(b []byte) (int, error) {
2✔
731
        n, err := ef.tmpf.Write(b)
2✔
732
        ef.flush = true
2✔
733
        return n, err
2✔
734
}
2✔
735

736
// Read reads all bytes from the filepath on the partition image into b
737
func (ef *extFile) Read(b []byte) (int, error) {
15✔
738
        str, err := debugfsCopyFile(ef.imageFilePath, ef.imagePath)
15✔
739
        defer os.RemoveAll(str) // ignore error removing tmp-dir
15✔
740
        if err != nil {
21✔
741
                return 0, errors.Wrap(err, "extFile: ReadError: debugfsCopyFile failed")
6✔
742
        }
6✔
743
        data, err := os.ReadFile(filepath.Join(str, filepath.Base(ef.imageFilePath)))
9✔
744
        if err != nil {
9✔
745
                return 0, errors.Wrapf(
×
746
                        err,
×
NEW
747
                        "extFile: ReadError: os.ReadFile failed to read file: %s",
×
748
                        filepath.Join(str, filepath.Base(ef.imageFilePath)),
×
749
                )
×
750
        }
×
751
        return copy(b, data), io.EOF
9✔
752
}
753

754
func (ef *extFile) CopyTo(hostFile string) error {
105✔
755
        if err := debugfsReplaceFile(ef.imageFilePath, hostFile, ef.imagePath); err != nil {
105✔
756
                return err
×
757
        }
×
758
        return nil
105✔
759
}
760

761
func (ef *extFile) CopyFrom(hostFile string) error {
19✔
762
        // Get the file permissions
19✔
763
        d, err := debugfsExecuteCommand(fmt.Sprintf("stat %s", ef.imageFilePath), ef.imagePath)
19✔
764
        if err != nil {
21✔
765
                if strings.Contains(err.Error(), "File not found by ext2_lookup") {
4✔
766
                        return fmt.Errorf("The file: %s does not exist in the image", ef.imageFilePath)
2✔
767
                }
2✔
768
                return err
×
769
        }
770
        // Extract the Mode: oooo octal code
771
        reg := regexp.MustCompile(`Mode: +(0[0-9]{3})`)
17✔
772
        m := reg.FindStringSubmatch(d.String())
17✔
773
        if m == nil || len(m) != 2 {
17✔
774
                return fmt.Errorf(
×
775
                        "Could not extract the filemode information from the file: %s\n",
×
776
                        ef.imageFilePath,
×
777
                )
×
778
        }
×
779
        mode, err := strconv.ParseInt(m[1], 8, 32)
17✔
780
        if err != nil {
17✔
781
                return fmt.Errorf(
×
782
                        "Failed to extract the file permissions for the file: %s\nerr: %s",
×
783
                        ef.imageFilePath,
×
784
                        err,
×
785
                )
×
786
        }
×
787
        _, err = debugfsExecuteCommand(
17✔
788
                fmt.Sprintf("dump %s %s\nclose", ef.imageFilePath, hostFile),
17✔
789
                ef.imagePath,
17✔
790
        )
17✔
791
        if err != nil {
17✔
792
                if strings.Contains(err.Error(), "File not found by ext2_lookup") {
×
793
                        return fmt.Errorf("The file: %s does not exist in the image", ef.imageFilePath)
×
794
                }
×
795
                return err
×
796
        }
797
        if err = os.Chmod(hostFile, os.FileMode(mode)); err != nil {
17✔
798
                return err
×
799
        }
×
800
        return nil
17✔
801
}
802

803
func (ef *extFile) Delete(recursive bool) (err error) {
18✔
804
        err = debugfsRemoveFileOrDir(ef.imageFilePath, ef.imagePath, recursive)
18✔
805
        if err != nil {
22✔
806
                return err
4✔
807
        }
4✔
808
        return nil
14✔
809
}
810

811
// Close closes the temporary file held by partitionFile path.
812
func (ef *extFile) Close() (err error) {
199✔
813
        if ef == nil {
199✔
814
                return nil
×
815
        }
×
816
        if ef.tmpf != nil {
398✔
817
                defer func() {
398✔
818
                        // Ignore tmp-errors
199✔
819
                        ef.tmpf.Close()
199✔
820
                        os.Remove(ef.tmpf.Name())
199✔
821
                }()
199✔
822
                if ef.flush {
201✔
823
                        err = debugfsReplaceFile(ef.imageFilePath, ef.tmpf.Name(), ef.imagePath)
2✔
824
                        if err != nil {
2✔
825
                                return err
×
826
                        }
×
827
                }
828
        }
829
        return err
199✔
830
}
831

832
func (ed *extDir) Create() error {
21✔
833
        err := debugfsMakeDir(ed.imageFilePath, ed.imagePath)
21✔
834
        return err
21✔
835
}
21✔
836

837
// Close closes the temporary file held by partitionFile path.
838
func (ed *extDir) Close() (err error) {
21✔
839
        if ed == nil {
21✔
840
                return nil
×
841
        }
×
842
        return err
21✔
843
}
844

845
// fatFile wraps a partition struct with a reader/writer for fat filesystems
846
type fatFile struct {
847
        imagePath     string
848
        imageFilePath string // The local filesystem path to the image
849
        flush         bool
850
        tmpf          *os.File
851
}
852

853
type fatDir struct {
854
        imagePath     string
855
        imageFilePath string
856
}
857

858
func newFatFile(imagePath, imageFilePath string) (*fatFile, error) {
15✔
859
        if err := runFsck(imagePath, "vfat"); err != nil {
15✔
860
                return nil, err
×
861
        }
×
862

863
        tmpf, err := os.CreateTemp("", "mendertmp-fatfile")
15✔
864
        ff := &fatFile{
15✔
865
                imagePath:     imagePath,
15✔
866
                imageFilePath: imageFilePath,
15✔
867
                tmpf:          tmpf,
15✔
868
        }
15✔
869
        return ff, err
15✔
870
}
871

872
func newFatDir(imagePath, imageFilePath string) (fd *fatDir, err error) {
×
873
        if err := runFsck(imagePath, "vfat"); err != nil {
×
874
                return nil, err
×
875
        }
×
876

877
        fd = &fatDir{
×
878
                imagePath:     imagePath,
×
879
                imageFilePath: imageFilePath,
×
880
        }
×
881
        return fd, err
×
882
}
883

884
// Read Dump the file contents to stdout, and capture, using MTools' mtype
885
func (f *fatFile) Read(b []byte) (n int, err error) {
1✔
886
        cmd := exec.Command("mtype", "-n", "-i", f.imagePath, "::"+f.imageFilePath)
1✔
887
        dbuf := bytes.NewBuffer(nil)
1✔
888
        cmd.Stdout = dbuf // capture Stdout
1✔
889
        if err = cmd.Run(); err != nil {
1✔
890
                return 0, errors.Wrap(err, "fatPartitionFile: Read: MTools mtype dump failed")
×
891
        }
×
892
        return copy(b, dbuf.Bytes()), io.EOF
1✔
893
}
894

895
// Write Writes to the underlying fat image, using MTools' mcopy
896
func (f *fatFile) Write(b []byte) (n int, err error) {
2✔
897
        n, err = f.tmpf.Write(b)
2✔
898
        if err != nil {
2✔
899
                return n, err
×
900
        }
×
901
        f.flush = true
2✔
902
        return n, nil
2✔
903
}
904

905
func (f *fatFile) CopyTo(hostFile string) error {
5✔
906
        cmd := exec.Command("mcopy", "-oi", f.imagePath, hostFile, "::"+f.imageFilePath)
5✔
907
        data := bytes.NewBuffer(nil)
5✔
908
        cmd.Stdout = data
5✔
909
        if err := cmd.Run(); err != nil {
5✔
910
                return errors.Wrap(err, "fatFile: Write: MTools execution failed")
×
911
        }
×
912
        return nil
5✔
913
}
914

915
func (f *fatFile) CopyFrom(hostFile string) error {
5✔
916
        cmd := exec.Command("mcopy", "-n", "-i", f.imagePath, "::"+f.imageFilePath, hostFile)
5✔
917
        dbuf := bytes.NewBuffer(nil)
5✔
918
        cmd.Stdout = dbuf // capture Stdout
5✔
919
        if err := cmd.Run(); err != nil {
6✔
920
                return errors.Wrap(err, "fatPartitionFile: Read: MTools mcopy failed")
1✔
921
        }
1✔
922
        return nil
4✔
923
}
924

925
func (f *fatFile) Delete(recursive bool) (err error) {
2✔
926
        isDir := filepath.Dir(f.imageFilePath) == strings.TrimRight(f.imageFilePath, "/")
2✔
927
        var deleteCmd string
2✔
928
        if isDir {
3✔
929
                deleteCmd = "mdeltree"
1✔
930
        } else {
2✔
931
                deleteCmd = "mdel"
1✔
932
        }
1✔
933
        cmd := exec.Command(deleteCmd, "-i", f.imagePath, "::"+f.imageFilePath)
2✔
934
        if err = cmd.Run(); err != nil {
2✔
935
                return errors.Wrap(err, "fatFile: Delete: execution failed: "+deleteCmd)
×
936
        }
×
937
        return nil
2✔
938
}
939

940
func (f *fatFile) Close() (err error) {
15✔
941
        if f == nil {
15✔
942
                return nil
×
943
        }
×
944
        if f.tmpf != nil {
30✔
945
                defer func() {
30✔
946
                        f.tmpf.Close()
15✔
947
                        os.Remove(f.tmpf.Name())
15✔
948
                }()
15✔
949
                if f.flush {
17✔
950
                        cmd := exec.Command(
2✔
951
                                "mcopy",
2✔
952
                                "-n",
2✔
953
                                "-i",
2✔
954
                                f.imagePath,
2✔
955
                                f.tmpf.Name(),
2✔
956
                                "::"+f.imageFilePath,
2✔
957
                        )
2✔
958
                        data := bytes.NewBuffer(nil)
2✔
959
                        cmd.Stdout = data
2✔
960
                        if err = cmd.Run(); err != nil {
3✔
961
                                return errors.Wrap(err, "fatFile: Write: MTools execution failed")
1✔
962
                        }
1✔
963
                }
964
        }
965
        return err
14✔
966
}
967

968
func (fd *fatDir) Create() (err error) {
×
969
        cmd := exec.Command("mmd", fd.imageFilePath)
×
970
        data := bytes.NewBuffer(nil)
×
971
        cmd.Stdout = data
×
972
        if err := cmd.Run(); err != nil {
×
973
                return errors.Wrap(err, "fatDir: Create: MTools execution failed")
×
974
        }
×
975
        return err
×
976
}
977

978
func (fd *fatDir) Close() (err error) {
×
979
        if fd == nil {
×
980
                return nil
×
981
        }
×
982
        os.Remove(fd.imagePath)
×
983
        return err
×
984
}
985

986
func processSdimg(image string) (VPImage, error) {
134✔
987
        bin, err := utils.GetBinaryPath("parted")
134✔
988
        if err != nil {
136✔
989
                return nil, errors.Wrap(err, "`parted` binary not found on the system")
2✔
990
        }
2✔
991
        out, err := exec.Command(bin, image, "unit s", "print").Output()
132✔
992
        if err != nil {
133✔
993
                return nil, errors.Wrap(err, "can not execute `parted` command or image is broken; "+
1✔
994
                        "make sure parted is available in your system and is in the $PATH")
1✔
995
        }
1✔
996

997
        reg := regexp.MustCompile(
131✔
998
                `(?m)^[[:blank:]][0-9]+[[:blank:]]+([0-9]+)s[[:blank:]]+[0-9]+s[[:blank:]]+([0-9]+)s`,
131✔
999
        )
131✔
1000
        partitionMatch := reg.FindAllStringSubmatch(string(out), -1)
131✔
1001

131✔
1002
        if len(partitionMatch) == 4 {
250✔
1003
                partitions := make([]partition, 0)
119✔
1004
                // we will have three groups per each entry in the partition table
119✔
1005
                for i := 0; i < 4; i++ {
595✔
1006
                        single := partitionMatch[i]
476✔
1007
                        partitions = append(partitions, partition{offset: single[1], size: single[2]})
476✔
1008
                }
476✔
1009
                if partitions, err = extractFromSdimg(partitions, image); err != nil {
119✔
1010
                        return nil, err
×
1011
                }
×
1012
                return &ModImageSdimg{
119✔
1013
                        ModImageBase: ModImageBase{
119✔
1014
                                path:  image,
119✔
1015
                                dirty: false,
119✔
1016
                        },
119✔
1017
                        candidates: partitions,
119✔
1018
                }, nil
119✔
1019
                // if we have single ext file there is no need to mount it
1020

1021
        } else if len(partitionMatch) == 1 && partitionMatch[0][1] == "0" {
24✔
1022
                // For one partition match which has an offset of zero, we
12✔
1023
                // assume it is a raw filesystem image.
12✔
1024
                return &ModImageRaw{
12✔
1025
                        ModImageBase: ModImageBase{
12✔
1026
                                path:  image,
12✔
1027
                                dirty: false,
12✔
1028
                        },
12✔
1029
                }, nil
12✔
1030
        }
12✔
1031
        return nil, fmt.Errorf("invalid partition table: %s", string(out))
×
1032
}
1033

1034
func extractFromSdimg(partitions []partition, image string) ([]partition, error) {
119✔
1035
        for i, part := range partitions {
595✔
1036
                tmp, err := os.CreateTemp("", "mender-modify-image")
476✔
1037
                if err != nil {
476✔
1038
                        return nil, errors.Wrap(err, "can not create temp file for storing image")
×
1039
                }
×
1040
                if err = tmp.Close(); err != nil {
476✔
1041
                        return nil, errors.Wrapf(err, "can not close temporary file: %s", tmp.Name())
×
1042
                }
×
1043
                cmd := exec.Command("dd", "if="+image, "of="+tmp.Name(),
476✔
1044
                        "skip="+part.offset, "count="+part.size)
476✔
1045
                if err = cmd.Run(); err != nil {
476✔
1046
                        return nil, errors.Wrap(err, "can not extract image from sdimg")
×
1047
                }
×
1048
                partitions[i].path = tmp.Name()
476✔
1049
        }
1050
        return partitions, nil
119✔
1051
}
1052

1053
func repackSdimg(partitions []partition, image string) error {
69✔
1054
        for _, part := range partitions {
345✔
1055
                if err := exec.Command("dd", "if="+part.path, "of="+image,
276✔
1056
                        "seek="+part.offset, "count="+part.size,
276✔
1057
                        "conv=notrunc").Run(); err != nil {
276✔
1058
                        return errors.Wrap(err, "can not copy image back to sdimg")
×
1059
                }
×
1060
        }
1061
        return nil
69✔
1062
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc