core/metainfo.go (122 lines of code) (raw):

// Copyright (c) 2016-2019 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package core import ( "bytes" "encoding/json" "errors" "fmt" "io" "github.com/jackpal/bencode-go" ) // info contains the "instructions" for how to download / seed a torrent, // primarily describing how a blob is broken up into pieces and how to verify // those pieces (i.e. the piece sums). type info struct { // Exported for bencoding. PieceLength int64 PieceSums []uint32 Name string Length int64 } // Hash computes the InfoHash of info. func (info *info) Hash() (InfoHash, error) { var b bytes.Buffer if err := bencode.Marshal(&b, *info); err != nil { return InfoHash{}, fmt.Errorf("bencode: %s", err) } return NewInfoHashFromBytes(b.Bytes()), nil } // MetaInfo contains torrent metadata. type MetaInfo struct { info info infoHash InfoHash digest Digest } // NewMetaInfo creates a new MetaInfo. Assumes that d is the valid digest for // blob (re-computing it is expensive). func NewMetaInfo(d Digest, blob io.Reader, pieceLength int64) (*MetaInfo, error) { length, pieceSums, err := calcPieceSums(blob, pieceLength) if err != nil { return nil, err } info := info{ PieceLength: pieceLength, PieceSums: pieceSums, Name: d.Hex(), Length: length, } h, err := info.Hash() if err != nil { return nil, fmt.Errorf("compute info hash: %s", err) } return &MetaInfo{ info: info, infoHash: h, digest: d, }, nil } // InfoHash returns the torrent InfoHash. func (mi *MetaInfo) InfoHash() InfoHash { return mi.infoHash } // Digest returns the digest of the original blob. func (mi *MetaInfo) Digest() Digest { return mi.digest } // Length returns the length of the original blob. func (mi *MetaInfo) Length() int64 { return mi.info.Length } // NumPieces returns the number of pieces in the torrent. func (mi *MetaInfo) NumPieces() int { return len(mi.info.PieceSums) } // PieceLength returns the piece length used to break up the original blob. Note, // the final piece may be shorter than this. Use GetPieceLength for the true // lengths of each piece. func (mi *MetaInfo) PieceLength() int64 { return mi.info.PieceLength } // GetPieceLength returns the length of piece i. func (mi *MetaInfo) GetPieceLength(i int) int64 { if i < 0 || i >= len(mi.info.PieceSums) { return 0 } if i == len(mi.info.PieceSums)-1 { // Last piece. return mi.info.Length - mi.info.PieceLength*int64(i) } return mi.info.PieceLength } // GetPieceSum returns the checksum of piece i. Does not check bounds. func (mi *MetaInfo) GetPieceSum(i int) uint32 { return mi.info.PieceSums[i] } // metaInfoJSON is used for serializing / deserializing MetaInfo. type metaInfoJSON struct { // Only serialize info for backwards compatibility. Info info `json:"Info"` } // Serialize converts mi to a json blob. func (mi *MetaInfo) Serialize() ([]byte, error) { return json.Marshal(&metaInfoJSON{mi.info}) } // DeserializeMetaInfo reconstructs a MetaInfo from a json blob. func DeserializeMetaInfo(data []byte) (*MetaInfo, error) { var j metaInfoJSON if err := json.Unmarshal(data, &j); err != nil { return nil, fmt.Errorf("json: %s", err) } h, err := j.Info.Hash() if err != nil { return nil, fmt.Errorf("compute info hash: %s", err) } d, err := NewSHA256DigestFromHex(j.Info.Name) if err != nil { return nil, fmt.Errorf("parse name: %s", err) } return &MetaInfo{ info: j.Info, infoHash: h, digest: d, }, nil } // calcPieceSums hashes blob content in pieceLength chunks. func calcPieceSums(blob io.Reader, pieceLength int64) (length int64, pieceSums []uint32, err error) { if pieceLength <= 0 { return 0, nil, errors.New("piece length must be positive") } for { h := PieceHash() n, err := io.CopyN(h, blob, pieceLength) if err != nil && err != io.EOF { return 0, nil, fmt.Errorf("read blob: %s", err) } length += n if n == 0 { break } sum := h.Sum32() pieceSums = append(pieceSums, sum) if n < pieceLength { break } } return length, pieceSums, nil }