diff --git a/zfs/zfs.go b/zfs/zfs.go index 2ed40f4c..a8dcf22c 100644 --- a/zfs/zfs.go +++ b/zfs/zfs.go @@ -1018,81 +1018,93 @@ type DrySendInfo struct { SizeEstimate uint64 // 0 if size estimate is not possible } -var ( - // keep same number of capture groups for unmarshalInfoLine homogeneity - - sendDryRunInfoLineRegexFull = regexp.MustCompile(`^(?Pfull)\t()(?P[^\t]+@[^\t]+)(\t(?P[0-9]+))?$`) - // cannot enforce '[#@]' in incremental source, see test cases - sendDryRunInfoLineRegexIncremental = regexp.MustCompile(`^(?Pincremental)\t(?P[^\t]+)\t(?P[^\t]+@[^\t]+)(\t(?P[0-9]+))?$`) -) - // see test cases for example output func (s *DrySendInfo) unmarshalZFSOutput(output []byte) error { debug("DrySendInfo.unmarshalZFSOutput: output=%q", output) scan := bufio.NewScanner(bytes.NewReader(output)) for scan.Scan() { l := scan.Text() - if regexMatched, err := s.unmarshalInfoLine(l); err != nil { + if err := s.unmarshalInfoLine(l); err != nil { return fmt.Errorf("line %q: %w", l, err) - } else if regexMatched { - return nil } } - return fmt.Errorf("no match for info line (regex1 %s) (regex2 %s)", - sendDryRunInfoLineRegexFull, sendDryRunInfoLineRegexIncremental) + + if s.Type != DrySendTypeFull && s.Type != DrySendTypeIncremental { + return fmt.Errorf("no match for info line in:\n%s", output) + } + return nil } // unmarshal info line, looks like this: // // full zroot/test/a@1 5389768 // incremental zroot/test/a@1 zroot/test/a@2 5383936 +// size 5383936 // // => see test cases -func (s *DrySendInfo) unmarshalInfoLine(l string) (regexMatched bool, err error) { - mFull := sendDryRunInfoLineRegexFull.FindStringSubmatch(l) - mInc := sendDryRunInfoLineRegexIncremental.FindStringSubmatch(l) - var matchingExpr *regexp.Regexp - var m []string - if mFull == nil && mInc == nil { - return false, nil - } else if mFull != nil && mInc != nil { - panic(fmt.Sprintf("ambiguous ZFS dry send output: %q", l)) - } else if mFull != nil { - matchingExpr, m = sendDryRunInfoLineRegexFull, mFull - } else if mInc != nil { - matchingExpr, m = sendDryRunInfoLineRegexIncremental, mInc - } - - fields := make(map[string]string, matchingExpr.NumSubexp()) - for i, name := range matchingExpr.SubexpNames() { - if i != 0 { - fields[name] = m[i] - } +func (s *DrySendInfo) unmarshalInfoLine(l string) error { + dryFields := strings.SplitN(l, "\t", 5) + n := len(dryFields) + if n == 0 { + return nil } - s.Type, err = DrySendTypeFromString(fields["type"]) - if err != nil { - return true, err + snapType := dryFields[0] + var from, to, size string + switch { + case snapType == "full" && n > 1: + to = dryFields[1] + if n > 2 { + size = dryFields[2] + } + case snapType == "incremental" && n > 2: + from, to = dryFields[1], dryFields[2] + if n > 3 { + size = dryFields[3] + } + if s.From == "" { + s.From = from + } + case snapType == "size" && n > 1: + size = dryFields[1] + default: + return nil } - s.From = fields["from"] - s.To = fields["to"] - toFS, _, _, err := DecomposeVersionString(s.To) - if err != nil { - return true, fmt.Errorf("'to' is not a valid filesystem version: %s", err) + if snapType == "full" || snapType == "incremental" { + if sendType, err := DrySendTypeFromString(snapType); err != nil { + return err + } else if s.Type == "" { + s.Type = sendType + } else if sendType != s.Type { + return fmt.Errorf("dry send type changed from %q to %q", s.Type, sendType) + } + s.To = to + if s.Filesystem == "" { + toFS, _, _, err := DecomposeVersionString(to) + if err != nil { + return fmt.Errorf("'to' is not a valid filesystem version: %s", err) + } + s.Filesystem = toFS + } + if size == "" { + // workaround for OpenZFS 0.7 prior to + // + // https://github.com/openzfs/zfs/commit/835db58592d7d947e5818eb7281882e2a46073e0#diff-66bd524398bcd2ac70d90925ab6d8073L1245 + // + // see https://github.com/zrepl/zrepl/issues/289 + return nil + } } - s.Filesystem = toFS - if fields["size"] == "" { - // workaround for OpenZFS 0.7 prior to https://github.com/openzfs/zfs/commit/835db58592d7d947e5818eb7281882e2a46073e0#diff-66bd524398bcd2ac70d90925ab6d8073L1245 - // see https://github.com/zrepl/zrepl/issues/289 - fields["size"] = "0" - } - s.SizeEstimate, err = strconv.ParseUint(fields["size"], 10, 64) - if err != nil { - return true, fmt.Errorf("cannot not parse size: %s", err) + if sizeEstimate, err := strconv.ParseUint(size, 10, 64); err != nil { + return fmt.Errorf("cannot not parse size %q: %s", size, err) + } else if snapType == "size" { + s.SizeEstimate = max(sizeEstimate, s.SizeEstimate) + } else { + s.SizeEstimate += sizeEstimate } - return true, nil + return nil } // to may be "", in which case a full ZFS send is done diff --git a/zfs/zfs_test.go b/zfs/zfs_test.go index 904708f0..b19496c4 100644 --- a/zfs/zfs_test.go +++ b/zfs/zfs_test.go @@ -93,10 +93,6 @@ func TestZFSPropertySource(t *testing.T) { } } -func TestDrySendRegexesHaveSameCaptureGroupCount(t *testing.T) { - assert.Equal(t, sendDryRunInfoLineRegexFull.NumSubexp(), sendDryRunInfoLineRegexIncremental.NumSubexp()) -} - func TestDrySendInfo(t *testing.T) { // # full send // $ zfs send -Pnv -t 1-9baebea70-b8-789c636064000310a500c4ec50360710e72765a52697303030419460caa7a515a79680647ce0f26c48f2499525a9c5405ac3c90fabfe92fcf4d2cc140686b30972c7850efd0cd24092e704cbe725e6a632305415e5e797e803cd2ad14f743084b805001b201795 @@ -179,7 +175,8 @@ full p1 with/ spaces d1@2 with space tcs := []tc{ { - name: "fullSend", in: fullSend, + name: "fullSend", + in: fullSend, exp: &DrySendInfo{ Type: DrySendTypeFull, Filesystem: "zroot/test/a", @@ -189,7 +186,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "incSend", in: incSend, + name: "incSend", + in: incSend, exp: &DrySendInfo{ Type: DrySendTypeIncremental, Filesystem: "zroot/test/a", @@ -199,7 +197,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "incSendBookmark", in: incSendBookmark, + name: "incSendBookmark", + in: incSendBookmark, exp: &DrySendInfo{ Type: DrySendTypeIncremental, Filesystem: "zroot/test/a", @@ -209,7 +208,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "incNoToken", in: incNoToken, + name: "incNoToken", + in: incNoToken, exp: &DrySendInfo{ Type: DrySendTypeIncremental, Filesystem: "zroot/test/a", @@ -221,7 +221,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "fullNoToken", in: fullNoToken, + name: "fullNoToken", + in: fullNoToken, exp: &DrySendInfo{ Type: DrySendTypeFull, Filesystem: "zroot/test/a", @@ -231,7 +232,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "fullWithSpaces", in: fullWithSpaces, + name: "fullWithSpaces", + in: fullWithSpaces, exp: &DrySendInfo{ Type: DrySendTypeFull, Filesystem: "pool1/otherjob/ds with spaces", @@ -241,7 +243,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "fullWithSpacesInIntermediateComponent", in: fullWithSpacesInIntermediateComponent, + name: "fullWithSpacesInIntermediateComponent", + in: fullWithSpacesInIntermediateComponent, exp: &DrySendInfo{ Type: DrySendTypeFull, Filesystem: "pool1/otherjob/another ds with spaces/childfs", @@ -251,7 +254,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "incrementalWithSpaces", in: incrementalWithSpaces, + name: "incrementalWithSpaces", + in: incrementalWithSpaces, exp: &DrySendInfo{ Type: DrySendTypeIncremental, Filesystem: "pool1/otherjob/another ds with spaces", @@ -261,7 +265,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "incrementalWithSpacesInIntermediateComponent", in: incrementalWithSpacesInIntermediateComponent, + name: "incrementalWithSpacesInIntermediateComponent", + in: incrementalWithSpacesInIntermediateComponent, exp: &DrySendInfo{ Type: DrySendTypeIncremental, Filesystem: "pool1/otherjob/another ds with spaces/childfs", @@ -271,7 +276,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "incrementalZeroSizedOpenZFS_pre0.7.12", in: incZeroSized_0_7_12, + name: "incrementalZeroSizedOpenZFS_pre0.7.12", + in: incZeroSized_0_7_12, exp: &DrySendInfo{ Type: DrySendTypeIncremental, Filesystem: "p1 with/ spaces d1", @@ -281,7 +287,8 @@ full p1 with/ spaces d1@2 with space }, }, { - name: "fullZeroSizedOpenZFS_pre0.7.12", in: fullZeroSized_0_7_12, + name: "fullZeroSizedOpenZFS_pre0.7.12", + in: fullZeroSized_0_7_12, exp: &DrySendInfo{ Type: DrySendTypeFull, Filesystem: "p1 with/ spaces d1", @@ -289,6 +296,89 @@ full p1 with/ spaces d1@2 with space SizeEstimate: 0, }, }, + { + name: "incremental package without size", + in: ` +incremental snap1 pool1/ds@snap2 624 +incremental snap2 pool1/ds@snap3 624 +`, + exp: &DrySendInfo{ + Type: DrySendTypeIncremental, + Filesystem: "pool1/ds", + From: "snap1", + To: "pool1/ds@snap3", + SizeEstimate: 1248, + }, + }, + { + name: "incremental package with size", + in: ` +incremental snap1 pool1/ds@snap2 624 +incremental snap2 pool1/ds@snap3 624 +size 1248 +`, + exp: &DrySendInfo{ + Type: DrySendTypeIncremental, + Filesystem: "pool1/ds", + From: "snap1", + To: "pool1/ds@snap3", + SizeEstimate: 1248, + }, + }, + { + name: "dry send type changed", + in: ` +full pool1/ds@snap2 624 +incremental snap2 pool1/ds@snap3 624 +size 1248 +`, + expErr: true, + }, + { + name: "no match for info line", + in: ` +size 1248 +`, + expErr: true, + }, + { + name: "cannot not parse size", + in: ` +incremental snap2 pool1/ds@snap3 624 +size XXX +`, + expErr: true, + }, + { + name: "incremental package with size less", + in: ` +incremental snap1 pool1/ds@snap2 624 +incremental snap2 pool1/ds@snap3 624 +size 1000 +`, + exp: &DrySendInfo{ + Type: DrySendTypeIncremental, + Filesystem: "pool1/ds", + From: "snap1", + To: "pool1/ds@snap3", + SizeEstimate: 1248, + }, + }, + { + name: "incremental package with size more", + in: ` +incremental snap1 pool1/ds@snap2 624 +incremental snap2 pool1/ds@snap3 624 +size 1500 +`, + exp: &DrySendInfo{ + Type: DrySendTypeIncremental, + Filesystem: "pool1/ds", + From: "snap1", + To: "pool1/ds@snap3", + SizeEstimate: 1500, + }, + }, } for _, tc := range tcs { @@ -300,9 +390,9 @@ full p1 with/ spaces d1@2 with space t.Logf("err=%T %s", err, err) if tc.expErr { - assert.Error(t, err) - } - if tc.exp != nil { + require.Error(t, err) + } else { + require.NoError(t, err) assert.Equal(t, tc.exp, &si) } })